From af0b3aa2b9f44ffd589a94402535ba380edc8d7b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 1 Dec 2020 18:19:03 +0100 Subject: [PATCH 1/4] MOBILE-3320 core: Use custom init status service --- src/core/classes/application-init-status.ts | 26 +++++++++++++++++++++ src/core/core.module.ts | 10 ++++---- 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/core/classes/application-init-status.ts diff --git a/src/core/classes/application-init-status.ts b/src/core/classes/application-init-status.ts new file mode 100644 index 000000000..c0e8850c8 --- /dev/null +++ b/src/core/classes/application-init-status.ts @@ -0,0 +1,26 @@ +// (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 { ApplicationInitStatus, APP_INITIALIZER, Injector } from '@angular/core'; +import { setSingletonsInjector } from '@singletons'; + +export class CoreApplicationInitStatus extends ApplicationInitStatus { + + constructor(injector: Injector) { + setSingletonsInjector(injector); + + super(injector.get(APP_INITIALIZER, [])); + } + +} diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 34ea13519..27d39f8b2 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -13,17 +13,17 @@ // limitations under the License. import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { Injector, NgModule } from '@angular/core'; +import { ApplicationInitStatus, Injector, NgModule } from '@angular/core'; import { Platform } from '@ionic/angular'; +import { CoreApplicationInitStatus } from './classes/application-init-status'; import { CoreFeaturesModule } from './features/features.module'; import { CoreFile } from './services/file'; import { CoreInit, CoreInitDelegate } from './services/init'; import { CoreInterceptor } from './classes/interceptor'; import { CoreSites, CORE_SITE_SCHEMAS } from './services/sites'; import { CoreUpdateManager } from './services/update-manager'; -import { setSingletonsInjector } from './singletons'; import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './services/db/filepool'; import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './services/db/sites'; import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; @@ -34,6 +34,7 @@ import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, + { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] }, { provide: CORE_SITE_SCHEMAS, useValue: [ @@ -47,10 +48,7 @@ import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; }) export class CoreModule { - constructor(platform: Platform, injector: Injector) { - // Set the injector. - setSingletonsInjector(injector); - + constructor(platform: Platform) { // Register a handler for platform ready. CoreInit.instance.registerProcess({ name: 'CorePlatformReady', From f1bc354be60b6d57e29b182db8ccd0b8320f7934 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 1 Dec 2020 18:35:07 +0100 Subject: [PATCH 2/4] MOBILE-3320 core: Refactor initializers --- package-lock.json | 6 + package.json | 1 + src/core/classes/application-init-status.ts | 3 +- src/core/core.module.ts | 47 +---- src/core/features/emulator/emulator.module.ts | 31 ++- .../emulator/services/emulator-helper.ts | 11 +- src/core/features/login/pages/init/init.ts | 5 +- .../features/login/tests/pages/init.test.ts | 7 +- src/core/guards/auth.ts | 4 +- src/core/initializers/clear-tmp-folder.ts | 19 ++ src/core/initializers/index.ts | 33 ++++ src/core/initializers/load-update-manager.ts | 19 ++ src/core/initializers/override-window-open.ts | 25 +++ .../initializers/prepare-automated-tests.ts | 38 ++++ src/core/initializers/restore-session.ts | 19 ++ .../subscribe-to-keyboard-events.ts | 28 +++ .../initializers/wait-for-platform-ready.ts | 19 ++ src/core/services/app.ts | 93 ++++----- src/core/services/cron.ts | 14 +- src/core/services/filepool.ts | 5 +- src/core/services/init.ts | 179 ------------------ src/core/services/update-manager.ts | 10 +- src/core/services/utils/utils.ts | 18 +- src/core/singletons/index.ts | 4 +- tsconfig.app.json | 3 +- tsconfig.json | 3 +- 26 files changed, 289 insertions(+), 355 deletions(-) create mode 100644 src/core/initializers/clear-tmp-folder.ts create mode 100644 src/core/initializers/index.ts create mode 100644 src/core/initializers/load-update-manager.ts create mode 100644 src/core/initializers/override-window-open.ts create mode 100644 src/core/initializers/prepare-automated-tests.ts create mode 100644 src/core/initializers/restore-session.ts create mode 100644 src/core/initializers/subscribe-to-keyboard-events.ts create mode 100644 src/core/initializers/wait-for-platform-ready.ts delete mode 100644 src/core/services/init.ts diff --git a/package-lock.json b/package-lock.json index cb5a21026..fdc609117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3312,6 +3312,12 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/webpack-env": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz", + "integrity": "sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw==", + "dev": true + }, "@types/webpack-sources": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", diff --git a/package.json b/package.json index d85aa2a26..553d0de6d 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@ionic/angular-toolkit": "^2.3.0", "@types/faker": "^5.1.3", "@types/node": "^12.12.64", + "@types/webpack-env": "^1.16.0", "@typescript-eslint/eslint-plugin": "4.3.0", "@typescript-eslint/parser": "4.3.0", "eslint": "^7.6.0", diff --git a/src/core/classes/application-init-status.ts b/src/core/classes/application-init-status.ts index c0e8850c8..f98420f81 100644 --- a/src/core/classes/application-init-status.ts +++ b/src/core/classes/application-init-status.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ApplicationInitStatus, APP_INITIALIZER, Injector } from '@angular/core'; +import { ApplicationInitStatus, APP_INITIALIZER, Injectable, Injector } from '@angular/core'; import { setSingletonsInjector } from '@singletons'; +@Injectable() export class CoreApplicationInitStatus extends ApplicationInitStatus { constructor(injector: Injector) { diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 27d39f8b2..df3bcffb0 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -15,18 +15,14 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ApplicationInitStatus, Injector, NgModule } from '@angular/core'; -import { Platform } from '@ionic/angular'; - import { CoreApplicationInitStatus } from './classes/application-init-status'; import { CoreFeaturesModule } from './features/features.module'; -import { CoreFile } from './services/file'; -import { CoreInit, CoreInitDelegate } from './services/init'; import { CoreInterceptor } from './classes/interceptor'; -import { CoreSites, CORE_SITE_SCHEMAS } from './services/sites'; -import { CoreUpdateManager } from './services/update-manager'; +import { CORE_SITE_SCHEMAS } from './services/sites'; import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './services/db/filepool'; import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './services/db/sites'; import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; +import { getInitializerProviders } from './initializers'; @NgModule({ imports: [ @@ -44,42 +40,7 @@ import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; ], multi: true, }, + ...getInitializerProviders(), ], }) -export class CoreModule { - - constructor(platform: Platform) { - // Register a handler for platform ready. - CoreInit.instance.registerProcess({ - name: 'CorePlatformReady', - priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 400, - blocking: true, - load: async () => { - await platform.ready(); - }, - }); - - // Register the update manager as an init process. - CoreInit.instance.registerProcess(CoreUpdateManager.instance); - - // Restore the user's session during the init process. - CoreInit.instance.registerProcess({ - name: 'CoreRestoreSession', - priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 200, - blocking: false, - load: CoreSites.instance.restoreSession.bind(CoreSites.instance), - }); - - // Register clear app tmp folder. - CoreInit.instance.registerProcess({ - name: 'CoreClearTmpFolder', - priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 150, - blocking: false, - load: CoreFile.instance.clearTmpFolder.bind(CoreFile.instance), - }); - - // Execute the init processes. - CoreInit.instance.executeInitProcesses(); - } - -} +export class CoreModule {} diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index da27adf03..c43fa19ec 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { Platform } from '@ionic/angular'; -import { CoreInitDelegate } from '@services/init'; import { CoreEmulatorHelperProvider } from './services/emulator-helper'; import { CoreEmulatorComponentsModule } from './components/components.module'; @@ -141,20 +140,18 @@ import { ZipMock } from './services/zip'; deps: [Platform, File], useFactory: (platform: Platform, file: File): Zip => platform.is('cordova') ? new Zip() : new ZipMock(file), }, + { + provide: APP_INITIALIZER, + deps: [Platform, CoreEmulatorHelperProvider], + useFactory: (platform: Platform, helperProvider: CoreEmulatorHelperProvider) => () => { + if (platform.is('cordova')) { + return; + } + + return helperProvider.load(); + }, + multi: true, + }, ], }) -export class CoreEmulatorModule { - - constructor( - platform: Platform, - initDelegate: CoreInitDelegate, - helper: CoreEmulatorHelperProvider, - ) { - - if (!platform.is('cordova')) { - // Register an init process to load the Mocks that need it. - initDelegate.registerProcess(helper); - } - } - -} +export class CoreEmulatorModule {} diff --git a/src/core/features/emulator/services/emulator-helper.ts b/src/core/features/emulator/services/emulator-helper.ts index 3e3f97ef5..618bc896a 100644 --- a/src/core/features/emulator/services/emulator-helper.ts +++ b/src/core/features/emulator/services/emulator-helper.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { File } from '@ionic-native/file/ngx'; import { CoreFile } from '@services/file'; -import { CoreInitDelegate, CoreInitHandler } from '@services/init'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; import { FileMock } from './file'; @@ -25,14 +24,8 @@ import { FileTransferErrorMock } from './file-transfer'; /** * Helper service for the emulator feature. It also acts as an init handler. */ -@Injectable({ - providedIn: 'root', -}) -export class CoreEmulatorHelperProvider implements CoreInitHandler { - - name = 'CoreEmulator'; - priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500; - blocking = true; +@Injectable({ providedIn: 'root' }) +export class CoreEmulatorHelperProvider { protected logger: CoreLogger; diff --git a/src/core/features/login/pages/init/init.ts b/src/core/features/login/pages/init/init.ts index 92688ca08..a1f726fac 100644 --- a/src/core/features/login/pages/init/init.ts +++ b/src/core/features/login/pages/init/init.ts @@ -16,8 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { NavController } from '@ionic/angular'; import { CoreApp, CoreRedirectData } from '@services/app'; -import { CoreInit } from '@services/init'; -import { SplashScreen } from '@singletons'; +import { ApplicationInit, SplashScreen } from '@singletons'; import { CoreConstants } from '@/core/constants'; import { CoreSites } from '@services/sites'; import { CoreLoginHelper } from '@features/login/services/login-helper'; @@ -39,7 +38,7 @@ export class CoreLoginInitPage implements OnInit { */ async ngOnInit(): Promise { // Wait for the app to be ready. - await CoreInit.instance.ready(); + await ApplicationInit.instance.donePromise; // Check if there was a pending redirect. const redirectData = CoreApp.instance.getRedirect(); diff --git a/src/core/features/login/tests/pages/init.test.ts b/src/core/features/login/tests/pages/init.test.ts index f2c64a6c1..fd135d94c 100644 --- a/src/core/features/login/tests/pages/init.test.ts +++ b/src/core/features/login/tests/pages/init.test.ts @@ -15,10 +15,9 @@ import { NavController } from '@ionic/angular'; import { CoreApp } from '@services/app'; -import { CoreInit } from '@services/init'; import { CoreLoginInitPage } from '@features/login/pages/init/init'; import { CoreSites } from '@services/sites'; -import { SplashScreen } from '@singletons'; +import { ApplicationInit, SplashScreen } from '@singletons'; import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils'; @@ -29,7 +28,7 @@ describe('CoreLoginInitPage', () => { beforeEach(() => { mockSingleton(CoreApp, { getRedirect: () => ({}) }); - mockSingleton(CoreInit, { ready: () => Promise.resolve() }); + mockSingleton(ApplicationInit, { donePromise: Promise.resolve() }); mockSingleton(CoreSites, { isLoggedIn: () => false }); mockSingleton(SplashScreen, ['hide']); @@ -52,7 +51,7 @@ describe('CoreLoginInitPage', () => { const fixture = await renderComponent(CoreLoginInitPage, config); fixture.componentInstance.ngOnInit(); - await CoreInit.instance.ready(); + await ApplicationInit.instance.donePromise; expect(navController.navigateRoot).toHaveBeenCalledWith('/login/sites'); }); diff --git a/src/core/guards/auth.ts b/src/core/guards/auth.ts index 9319a4b36..3c90cfff4 100644 --- a/src/core/guards/auth.ts +++ b/src/core/guards/auth.ts @@ -15,8 +15,8 @@ import { Injectable } from '@angular/core'; import { Router, CanLoad, CanActivate, UrlTree } from '@angular/router'; -import { CoreInit } from '@services/init'; import { CoreSites } from '@services/sites'; +import { ApplicationInit } from '@singletons'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanLoad, CanActivate { @@ -32,7 +32,7 @@ export class AuthGuard implements CanLoad, CanActivate { } private async guard(): Promise { - await CoreInit.instance.ready(); + await ApplicationInit.instance.donePromise; return CoreSites.instance.isLoggedIn() || this.router.parseUrl('/login'); } diff --git a/src/core/initializers/clear-tmp-folder.ts b/src/core/initializers/clear-tmp-folder.ts new file mode 100644 index 000000000..1d420c298 --- /dev/null +++ b/src/core/initializers/clear-tmp-folder.ts @@ -0,0 +1,19 @@ +// (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 { CoreFile } from '@services/file'; + +export default async function(): Promise { + await CoreFile.instance.clearTmpFolder(); +} diff --git a/src/core/initializers/index.ts b/src/core/initializers/index.ts new file mode 100644 index 000000000..f9f60ea14 --- /dev/null +++ b/src/core/initializers/index.ts @@ -0,0 +1,33 @@ +// (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 { APP_INITIALIZER, Provider } from '@angular/core'; + +export function getInitializerProviders(): Provider[] { + const context = require.context('./', false, /\.ts$/); + + return context.keys().reduce((providers, fileName) => { + const name = (fileName.match(/^(?:\.\/)?(.+)\.ts$/) || [])[1]; + + if (typeof name !== 'undefined' && name !== 'index') { + providers.push({ + provide: APP_INITIALIZER, + useValue: context(fileName).default, + multi: true, + }); + } + + return providers; + }, [] as Provider[]); +} diff --git a/src/core/initializers/load-update-manager.ts b/src/core/initializers/load-update-manager.ts new file mode 100644 index 000000000..d5a530d2b --- /dev/null +++ b/src/core/initializers/load-update-manager.ts @@ -0,0 +1,19 @@ +// (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 { CoreUpdateManager } from '@services/update-manager'; + +export default async function(): Promise { + await CoreUpdateManager.instance.load(); +} diff --git a/src/core/initializers/override-window-open.ts b/src/core/initializers/override-window-open.ts new file mode 100644 index 000000000..78e7c0bf5 --- /dev/null +++ b/src/core/initializers/override-window-open.ts @@ -0,0 +1,25 @@ +// (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 { Platform } from '@singletons'; + +export default async function(): Promise { + await Platform.instance.ready(); + + if (!window.cordova?.InAppBrowser) { + return; + } + + window.open = window.cordova.InAppBrowser.open; +} diff --git a/src/core/initializers/prepare-automated-tests.ts b/src/core/initializers/prepare-automated-tests.ts new file mode 100644 index 000000000..68b3298f5 --- /dev/null +++ b/src/core/initializers/prepare-automated-tests.ts @@ -0,0 +1,38 @@ +// (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 { ApplicationRef } from '@angular/core'; +import { CoreApp, CoreAppProvider } from '@services/app'; +import { CoreCron, CoreCronDelegate } from '@services/cron'; +import { Application } from '@singletons'; + +type AutomatedTestsWindow = Window & { + appRef?: ApplicationRef; + appProvider?: CoreAppProvider; + cronProvider?: CoreCronDelegate; +}; + +function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) { + window.appRef = Application.instance; + window.appProvider = CoreApp.instance; + window.cronProvider = CoreCron.instance; +} + +export default function(): void { + if (!CoreAppProvider.isAutomated()) { + return; + } + + initializeAutomatedTestsWindow(window); +} diff --git a/src/core/initializers/restore-session.ts b/src/core/initializers/restore-session.ts new file mode 100644 index 000000000..55a6b1ad7 --- /dev/null +++ b/src/core/initializers/restore-session.ts @@ -0,0 +1,19 @@ +// (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 { CoreSites } from '@services/sites'; + +export default async function(): Promise { + await CoreSites.instance.restoreSession(); +} diff --git a/src/core/initializers/subscribe-to-keyboard-events.ts b/src/core/initializers/subscribe-to-keyboard-events.ts new file mode 100644 index 000000000..9bb7deb14 --- /dev/null +++ b/src/core/initializers/subscribe-to-keyboard-events.ts @@ -0,0 +1,28 @@ +// (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 { CoreApp } from '@services/app'; +import { NgZone, Keyboard } from '@singletons'; + +export default function(): void { + const zone = NgZone.instance; + const app = CoreApp.instance; + const keyboard = Keyboard.instance; + + // Execute callbacks in the Angular zone, so change detection doesn't stop working. + keyboard.onKeyboardShow().subscribe(data => zone.run(() => app.onKeyboardShow(data.keyboardHeight))); + keyboard.onKeyboardHide().subscribe(() => zone.run(() => app.onKeyboardHide())); + keyboard.onKeyboardWillShow().subscribe(() => zone.run(() => app.onKeyboardWillShow())); + keyboard.onKeyboardWillHide().subscribe(() => zone.run(() => app.onKeyboardWillHide())); +} diff --git a/src/core/initializers/wait-for-platform-ready.ts b/src/core/initializers/wait-for-platform-ready.ts new file mode 100644 index 000000000..d0ad780c2 --- /dev/null +++ b/src/core/initializers/wait-for-platform-ready.ts @@ -0,0 +1,19 @@ +// (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 { Platform } from '@singletons'; + +export default async function(): Promise { + await Platform.instance.ready(); +} diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 4c334be1c..f6ea7a4fb 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, NgZone, ApplicationRef } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Params, Router } from '@angular/router'; import { Connection } from '@ionic-native/network/ngx'; @@ -57,60 +57,17 @@ export class CoreAppProvider { // Variables for DB. protected createVersionsTableReady: Promise; - constructor( - appRef: ApplicationRef, - zone: NgZone, - protected router: Router, - ) { + constructor(protected router: Router) { this.logger = CoreLogger.getInstance('CoreAppProvider'); this.db = CoreDB.instance.getDB(DBNAME); // Create the schema versions table. this.createVersionsTableReady = this.db.createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA); - Keyboard.instance.onKeyboardShow().subscribe((data) => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - zone.run(() => { - document.body.classList.add('keyboard-is-open'); - this.setKeyboardShown(true); - // Error on iOS calculating size. - // More info: https://github.com/ionic-team/ionic-plugin-keyboard/issues/276 . - CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, data.keyboardHeight); - }); - }); - Keyboard.instance.onKeyboardHide().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - zone.run(() => { - document.body.classList.remove('keyboard-is-open'); - this.setKeyboardShown(false); - CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, 0); - }); - }); - Keyboard.instance.onKeyboardWillShow().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - zone.run(() => { - this.keyboardOpening = true; - this.keyboardClosing = false; - }); - }); - Keyboard.instance.onKeyboardWillHide().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - zone.run(() => { - this.keyboardOpening = false; - this.keyboardClosing = true; - }); - }); - // @todo // this.platform.registerBackButtonAction(() => { // this.backButtonAction(); // }, 100); - - // Export the app provider and appRef to control the application in Behat tests. - if (CoreAppProvider.isAutomated()) { - ( window).appProvider = this; - ( window).appRef = appRef; - } } /** @@ -429,6 +386,44 @@ export class CoreAppProvider { } } + /** + * Notify that Keyboard has been shown. + * + * @param keyboardHeight Keyboard height. + */ + onKeyboardShow(keyboardHeight: number): void { + document.body.classList.add('keyboard-is-open'); + this.setKeyboardShown(true); + // Error on iOS calculating size. + // More info: https://github.com/ionic-team/ionic-plugin-keyboard/issues/276 . + CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, keyboardHeight); + } + + /** + * Notify that Keyboard has been hidden. + */ + onKeyboardHide(): void { + document.body.classList.remove('keyboard-is-open'); + this.setKeyboardShown(false); + CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, 0); + } + + /** + * Notify that Keyboard is about to be shown. + */ + onKeyboardWillShow(): void { + this.keyboardOpening = true; + this.keyboardClosing = false; + } + + /** + * Notify that Keyboard is about to be hidden. + */ + onKeyboardWillHide(): void { + this.keyboardOpening = false; + this.keyboardClosing = true; + } + /** * Set keyboard shown or hidden. * @@ -735,11 +730,3 @@ export type CoreAppSchema = { */ migrate?(db: SQLiteDB, oldVersion: number): Promise; }; - -/** - * Extended window type for automated tests. - */ -export type WindowForAutomatedTests = Window & { - appProvider?: CoreAppProvider; - appRef?: ApplicationRef; -}; diff --git a/src/core/services/cron.ts b/src/core/services/cron.ts index 8cc5ec81e..705b80cd7 100644 --- a/src/core/services/cron.ts +++ b/src/core/services/cron.ts @@ -14,7 +14,7 @@ import { Injectable, NgZone } from '@angular/core'; -import { CoreApp, CoreAppProvider } from '@services/app'; +import { CoreApp } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; @@ -57,11 +57,6 @@ export class CoreCronDelegate { this.startNetworkHandlers(); }); }); - - // Export the sync provider so Behat tests can trigger cron tasks without waiting. - if (CoreAppProvider.isAutomated()) { - ( window).cronProvider = this; - } } /** @@ -532,10 +527,3 @@ export interface CoreCronHandler { */ execute?(siteId?: string, force?: boolean): Promise; } - -/** - * Extended window type for automated tests. - */ -export type WindowForAutomatedTests = Window & { - cronProvider?: CoreCronDelegate; -}; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 28b441509..9ab08dcb2 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -18,7 +18,6 @@ import { Md5 } from 'ts-md5/dist/md5'; import { CoreApp } from '@services/app'; import { CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; -import { CoreInit } from '@services/init'; import { CorePluginFile } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSExternalFile } from '@services/ws'; @@ -31,7 +30,7 @@ import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; -import { makeSingleton, Network, NgZone, Translate } from '@singletons'; +import { ApplicationInit, makeSingleton, Network, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { APP_SCHEMA, @@ -108,7 +107,7 @@ export class CoreFilepoolProvider { */ protected async init(): Promise { // Waiting for the app to be ready to start processing the queue. - await CoreInit.instance.ready(); + await ApplicationInit.instance.donePromise; this.checkQueueProcessing(); diff --git a/src/core/services/init.ts b/src/core/services/init.ts deleted file mode 100644 index e391fc6f4..000000000 --- a/src/core/services/init.ts +++ /dev/null @@ -1,179 +0,0 @@ -// (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 { Injectable } from '@angular/core'; - -import { CoreUtils, PromiseDefer, OrderedPromiseData } from '@services/utils/utils'; -import { CoreLogger } from '@singletons/logger'; -import { makeSingleton } from '@singletons'; - -/** - * Interface that all init handlers must implement. - */ -export type CoreInitHandler = { - /** - * A name to identify the handler. - */ - name: string; - - /** - * The highest priority is executed first. You should use values lower than MAX_RECOMMENDED_PRIORITY. - */ - priority?: number; - - /** - * Set this to true when this process should be resolved before any following one. - */ - blocking?: boolean; - - /** - * Function to execute during the init process. - * - * @return Promise resolved when done. - */ - load(): Promise; -}; - -/* - * Provider for initialisation mechanisms. - */ -@Injectable({ providedIn: 'root' }) -export class CoreInitDelegate { - - static readonly DEFAULT_PRIORITY = 100; // Default priority for init processes. - static readonly MAX_RECOMMENDED_PRIORITY = 600; - - protected initProcesses: { [s: string]: CoreInitHandler } = {}; - protected logger: CoreLogger; - protected readiness?: CoreInitReadinessPromiseDefer; - - constructor() { - this.logger = CoreLogger.getInstance('CoreInitDelegate'); - } - - /** - * Executes the registered init processes. - * - * Reserved for core use, do not call directly. - */ - executeInitProcesses(): void { - const ordered: CoreInitHandler[] = []; - - if (typeof this.readiness == 'undefined') { - this.initReadiness(); - } - - // Re-ordering by priority. - for (const name in this.initProcesses) { - ordered.push(this.initProcesses[name]); - } - ordered.sort((a, b) => (b.priority || 0) - (a.priority || 0)); - - const orderedPromises: OrderedPromiseData[] = ordered.map((data: CoreInitHandler) => ({ - function: this.prepareProcess.bind(this, data), - blocking: !!data.blocking, - })); - - // Execute all the processes in order to solve dependencies. - CoreUtils.instance.executeOrderedPromises(orderedPromises).finally(this.readiness!.resolve); - } - - /** - * Init the readiness promise. - */ - protected initReadiness(): void { - this.readiness = CoreUtils.instance.promiseDefer(); - - // eslint-disable-next-line promise/catch-or-return - this.readiness.promise.then(() => this.readiness!.resolved = true); - } - - /** - * Instantly returns if the app is ready. - * - * @return Whether it's ready. - */ - isReady(): boolean { - return this.readiness?.resolved || false; - } - - /** - * Convenience function to return a function that executes the process. - * - * @param data The data of the process. - * @return Promise of the process. - */ - protected async prepareProcess(data: CoreInitHandler): Promise { - this.logger.debug(`Executing init process '${data.name}'`); - - try { - await data.load(); - } catch (e) { - this.logger.error('Error while calling the init process \'' + data.name + '\'. ' + e); - } - } - - /** - * Notifies when the app is ready. This returns a promise that is resolved when the app is initialised. - * - * @return Resolved when the app is initialised. Never rejected. - */ - async ready(): Promise { - if (typeof this.readiness == 'undefined') { - // Prevent race conditions if this is called before executeInitProcesses. - this.initReadiness(); - } - - await this.readiness!.promise; - } - - /** - * Registers an initialisation process. - * - * @description - * Init processes can be used to add initialisation logic to the app. Anything that should block the user interface while - * some processes are done should be an init process. It is recommended to use a priority lower than MAX_RECOMMENDED_PRIORITY - * to make sure that your process does not happen before some essential other core processes. - * - * An init process should never change state or prompt user interaction. - * - * This delegate cannot be used by site plugins. - * - * @param instance The instance of the handler. - */ - registerProcess(handler: CoreInitHandler): void { - if (typeof handler.priority == 'undefined') { - handler.priority = CoreInitDelegate.DEFAULT_PRIORITY; - } - - if (typeof this.initProcesses[handler.name] != 'undefined') { - this.logger.log(`Process '${handler.name}' already registered.`); - - return; - } - - this.logger.log(`Registering process '${handler.name}'.`); - this.initProcesses[handler.name] = handler; - } - -} - -export class CoreInit extends makeSingleton(CoreInitDelegate) {} - -/** - * Deferred promise for init readiness. - */ -type CoreInitReadinessPromiseDefer = PromiseDefer & { - resolved?: boolean; // If true, readiness have been resolved. -}; diff --git a/src/core/services/update-manager.ts b/src/core/services/update-manager.ts index 453a79777..3963db5d4 100644 --- a/src/core/services/update-manager.ts +++ b/src/core/services/update-manager.ts @@ -15,10 +15,9 @@ import { Injectable } from '@angular/core'; import { CoreConfig } from '@services/config'; -import { CoreInitHandler, CoreInitDelegate } from '@services/init'; import { CoreConstants } from '@/core/constants'; -import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; +import { makeSingleton } from '@singletons'; const VERSION_APPLIED = 'version_applied'; @@ -28,12 +27,7 @@ const VERSION_APPLIED = 'version_applied'; * This service handles processes that need to be run when updating the app, like migrate Ionic 1 database data to Ionic 3. */ @Injectable({ providedIn: 'root' }) -export class CoreUpdateManagerProvider implements CoreInitHandler { - - // Data for init delegate. - name = 'CoreUpdateManager'; - priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 300; - blocking = true; +export class CoreUpdateManagerProvider { protected logger: CoreLogger; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 6c376419d..b554e0999 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -26,9 +26,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreWSError } from '@classes/errors/wserror'; -import { - makeSingleton, Clipboard, InAppBrowser, Platform, FileOpener, WebIntent, QRScanner, Translate, -} from '@singletons'; +import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, QRScanner, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; type TreeNode = T & { children: TreeNode[] }; @@ -48,9 +46,6 @@ export class CoreUtilsProvider { constructor(protected zone: NgZone) { this.logger = CoreLogger.getInstance('CoreUtilsProvider'); - - // eslint-disable-next-line promise/catch-or-return - Platform.instance.ready().then(() => this.overrideWindowOpen()); } /** @@ -1606,17 +1601,6 @@ export class CoreUtilsProvider { return new Promise(resolve => setTimeout(resolve, milliseconds)); } - /** - * Override native window.open with InAppBrowser if available. - */ - private overrideWindowOpen() { - if (!window.cordova?.InAppBrowser) { - return; - } - - window.open = window.cordova.InAppBrowser.open; - } - } export class CoreUtils extends makeSingleton(CoreUtilsProvider) {} diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 61c539be1..161f310ae 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injector, NgZone as NgZoneService } from '@angular/core'; +import { ApplicationRef, ApplicationInitStatus, Injector, NgZone as NgZoneService } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { @@ -111,6 +111,8 @@ export class LoadingController extends makeSingleton(LoadingControllerService) { export class ModalController extends makeSingleton(ModalControllerService) {} export class ToastController extends makeSingleton(ToastControllerService) {} export class GestureController extends makeSingleton(GestureControllerService) {} +export class ApplicationInit extends makeSingleton(ApplicationInitStatus) {} +export class Application extends makeSingleton(ApplicationRef) {} // Convert external libraries injectables. export class Translate extends makeSingleton(TranslateService) {} diff --git a/tsconfig.app.json b/tsconfig.app.json index 1ec2414f0..69cd4cf31 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -7,7 +7,8 @@ "cordova-plugin-inappbrowser", "cordova", "dom-mediacapture-record", - "node" + "node", + "webpack-env" ], "paths": { "@classes/*": ["core/classes/*"], diff --git a/tsconfig.json b/tsconfig.json index b5c383525..f94ebb411 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "dom-mediacapture-record", "faker", "jest", - "node" + "node", + "webpack-env" ], "paths": { "@classes/*": ["core/classes/*"], From 17dbc340e0c00ec28e79bdfb29f50809542dd3f0 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 1 Dec 2020 18:37:24 +0100 Subject: [PATCH 3/4] MOBILE-3320 core: Refactor database inits --- src/core/core.module.ts | 15 +- src/core/features/course/course.module.ts | 2 +- .../course/services/course-offline.ts | 2 +- src/core/features/course/services/course.ts | 2 +- .../services/{db => database}/course.ts | 0 src/core/initializers/initialize-databases.ts | 31 +++++ src/core/services/app.ts | 61 ++++++--- src/core/services/config.ts | 34 +++-- src/core/services/cron.ts | 39 ++++-- src/core/services/{db => database}/app.ts | 0 src/core/services/{db => database}/config.ts | 0 src/core/services/{db => database}/cron.ts | 0 .../services/{db => database}/filepool.ts | 0 src/core/services/database/index.ts | 32 +++++ .../{db => database}/local-notifications.ts | 0 src/core/services/{db => database}/sites.ts | 0 src/core/services/{db => database}/sync.ts | 0 src/core/services/filepool.ts | 53 ++++--- src/core/services/local-notifications.ts | 53 ++++--- src/core/services/sites.ts | 129 +++++++++--------- src/core/services/sync.ts | 2 +- 21 files changed, 282 insertions(+), 173 deletions(-) rename src/core/features/course/services/{db => database}/course.ts (100%) create mode 100644 src/core/initializers/initialize-databases.ts rename src/core/services/{db => database}/app.ts (100%) rename src/core/services/{db => database}/config.ts (100%) rename src/core/services/{db => database}/cron.ts (100%) rename src/core/services/{db => database}/filepool.ts (100%) create mode 100644 src/core/services/database/index.ts rename src/core/services/{db => database}/local-notifications.ts (100%) rename src/core/services/{db => database}/sites.ts (100%) rename src/core/services/{db => database}/sync.ts (100%) diff --git a/src/core/core.module.ts b/src/core/core.module.ts index df3bcffb0..55dd2b93d 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -18,10 +18,7 @@ import { ApplicationInitStatus, Injector, NgModule } from '@angular/core'; import { CoreApplicationInitStatus } from './classes/application-init-status'; import { CoreFeaturesModule } from './features/features.module'; import { CoreInterceptor } from './classes/interceptor'; -import { CORE_SITE_SCHEMAS } from './services/sites'; -import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './services/db/filepool'; -import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './services/db/sites'; -import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './services/db/sync'; +import { getDatabaseProviders } from './services/database'; import { getInitializerProviders } from './initializers'; @NgModule({ @@ -31,15 +28,7 @@ import { getInitializerProviders } from './initializers'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] }, - { - provide: CORE_SITE_SCHEMAS, - useValue: [ - FILEPOOL_SITE_SCHEMA, - SITES_SITE_SCHEMA, - SYNC_SITE_SCHEMA, - ], - multi: true, - }, + ...getDatabaseProviders(), ...getInitializerProviders(), ], }) diff --git a/src/core/features/course/course.module.ts b/src/core/features/course/course.module.ts index d63ef7568..728272224 100644 --- a/src/core/features/course/course.module.ts +++ b/src/core/features/course/course.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { CORE_SITE_SCHEMAS } from '@services/sites'; -import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/db/course'; +import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course'; @NgModule({ providers: [ diff --git a/src/core/features/course/services/course-offline.ts b/src/core/features/course/services/course-offline.ts index 406f149ea..3be751813 100644 --- a/src/core/features/course/services/course-offline.ts +++ b/src/core/features/course/services/course-offline.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { makeSingleton } from '@singletons'; import { CoreSites } from '@services/sites'; -import { CoreCourseManualCompletionDBRecord, MANUAL_COMPLETION_TABLE } from './db/course'; +import { CoreCourseManualCompletionDBRecord, MANUAL_COMPLETION_TABLE } from './database/course'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; /** diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 0ae0d5c57..40fa21bce 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -26,7 +26,7 @@ import { CoreConstants } from '@/core/constants'; import { makeSingleton, Translate } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile } from '@services/ws'; -import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './db/course'; +import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course'; import { CoreCourseOffline } from './course-offline'; import { CoreError } from '@classes/errors/error'; import { diff --git a/src/core/features/course/services/db/course.ts b/src/core/features/course/services/database/course.ts similarity index 100% rename from src/core/features/course/services/db/course.ts rename to src/core/features/course/services/database/course.ts diff --git a/src/core/initializers/initialize-databases.ts b/src/core/initializers/initialize-databases.ts new file mode 100644 index 000000000..462aeaeef --- /dev/null +++ b/src/core/initializers/initialize-databases.ts @@ -0,0 +1,31 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreConfig } from '@services/config'; +import { CoreCron } from '@services/cron'; +import { CoreFilepool } from '@services/filepool'; +import { CoreLocalNotifications } from '@services/local-notifications'; +import { CoreSites } from '@services/sites'; + +export default async function(): Promise { + await Promise.all([ + CoreApp.instance.initialiseDatabase(), + CoreConfig.instance.initialiseDatabase(), + CoreCron.instance.initialiseDatabase(), + CoreFilepool.instance.initialiseDatabase(), + CoreLocalNotifications.instance.initialiseDatabase(), + CoreSites.instance.initialiseDatabase(), + ]); +} diff --git a/src/core/services/app.ts b/src/core/services/app.ts index f6ea7a4fb..945b9dc4b 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -25,7 +25,15 @@ import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreColors } from '@singletons/colors'; -import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/db/app'; +import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; + +/** + * Object responsible of managing schema versions. + */ +type SchemaVersionsManager = { + get(schemaName: string): Promise; + set(schemaName: string, version: number): Promise; +}; /** * Factory to provide some global functionalities, like access to the global app database. @@ -55,14 +63,13 @@ export class CoreAppProvider { protected forceOffline = false; // Variables for DB. - protected createVersionsTableReady: Promise; + protected schemaVersionsManager: Promise; + protected resolveSchemaVersionsManager!: (schemaVersionsManager: SchemaVersionsManager) => void; constructor(protected router: Router) { - this.logger = CoreLogger.getInstance('CoreAppProvider'); + this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve); this.db = CoreDB.instance.getDB(DBNAME); - - // Create the schema versions table. - this.createVersionsTableReady = this.db.createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA); + this.logger = CoreLogger.getInstance('CoreAppProvider'); // @todo // this.platform.registerBackButtonAction(() => { @@ -79,6 +86,30 @@ export class CoreAppProvider { return !!navigator.webdriver; } + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + await this.db.createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA); + + this.resolveSchemaVersionsManager({ + get: async name => { + try { + // Fetch installed version of the schema. + const entry = await this.db.getRecord(SCHEMA_VERSIONS_TABLE_NAME, { name }); + + return entry.version; + } catch (error) { + // No installed version yet. + return 0; + } + }, + set: async (name, version) => { + await this.db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name, version }); + }, + }); + } + /** * Check if the browser supports mediaDevices.getUserMedia. * @@ -115,20 +146,8 @@ export class CoreAppProvider { async createTablesFromSchema(schema: CoreAppSchema): Promise { this.logger.debug(`Apply schema to app DB: ${schema.name}`); - let oldVersion: number; - - try { - // Wait for the schema versions table to be created. - await this.createVersionsTableReady; - - // Fetch installed version of the schema. - const entry = await this.db.getRecord(SCHEMA_VERSIONS_TABLE_NAME, { name: schema.name }); - - oldVersion = entry.version; - } catch (error) { - // No installed version yet. - oldVersion = 0; - } + const schemaVersionsManager = await this.schemaVersionsManager; + const oldVersion = await schemaVersionsManager.get(schema.name); if (oldVersion >= schema.version) { // Version already installed, nothing else to do. @@ -145,7 +164,7 @@ export class CoreAppProvider { } // Set installed version. - await this.db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name: schema.name, version: schema.version }); + schemaVersionsManager.set(schema.name, schema.version); } /** diff --git a/src/core/services/config.ts b/src/core/services/config.ts index a5979f4d6..a39e593ba 100644 --- a/src/core/services/config.ts +++ b/src/core/services/config.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreApp } from '@services/app'; import { SQLiteDB } from '@classes/sqlitedb'; import { makeSingleton } from '@singletons'; -import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/db/config'; +import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config'; /** * Factory to provide access to dynamic and permanent config and settings. @@ -26,14 +26,24 @@ import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/db/confi @Injectable({ providedIn: 'root' }) export class CoreConfigProvider { - protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; constructor() { - this.appDB = CoreApp.instance.getDB(); - this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => { + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); + } + + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + try { + await CoreApp.instance.createTablesFromSchema(APP_SCHEMA); + } catch (e) { // Ignore errors. - }); + } + + this.resolveAppDB(CoreApp.instance.getDB()); } /** @@ -43,9 +53,9 @@ export class CoreConfigProvider { * @return Promise resolved when done. */ async delete(name: string): Promise { - await this.dbReady; + const db = await this.appDB; - await this.appDB.deleteRecords(CONFIG_TABLE_NAME, { name }); + await db.deleteRecords(CONFIG_TABLE_NAME, { name }); } /** @@ -56,10 +66,10 @@ export class CoreConfigProvider { * @return Resolves upon success along with the config data. Reject on failure. */ async get(name: string, defaultValue?: T): Promise { - await this.dbReady; + const db = await this.appDB; try { - const entry = await this.appDB.getRecord(CONFIG_TABLE_NAME, { name }); + const entry = await db.getRecord(CONFIG_TABLE_NAME, { name }); return entry.value; } catch (error) { @@ -79,9 +89,9 @@ export class CoreConfigProvider { * @return Promise resolved when done. */ async set(name: string, value: number | string): Promise { - await this.dbReady; + const db = await this.appDB; - await this.appDB.insertRecord(CONFIG_TABLE_NAME, { name, value }); + await db.insertRecord(CONFIG_TABLE_NAME, { name, value }); } } diff --git a/src/core/services/cron.ts b/src/core/services/cron.ts index 705b80cd7..bf799d90c 100644 --- a/src/core/services/cron.ts +++ b/src/core/services/cron.ts @@ -23,7 +23,7 @@ import { CoreError } from '@classes/errors/error'; import { makeSingleton, Network } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/db/cron'; +import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/database/cron'; /* * Service to handle cron processes. The registered processes will be executed every certain time. @@ -37,18 +37,16 @@ export class CoreCronDelegate { static readonly MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes. protected logger: CoreLogger; - protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected handlers: { [s: string]: CoreCronHandler } = {}; protected queuePromise: Promise = Promise.resolve(); - constructor(zone: NgZone) { - this.logger = CoreLogger.getInstance('CoreCronDelegate'); + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; - this.appDB = CoreApp.instance.getDB(); - this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => { - // Ignore errors. - }); + constructor(zone: NgZone) { + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); + this.logger = CoreLogger.getInstance('CoreCronDelegate'); // When the app is re-connected, start network handlers that were stopped. Network.instance.onConnect().subscribe(() => { @@ -59,6 +57,19 @@ export class CoreCronDelegate { }); } + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + try { + await CoreApp.instance.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + this.resolveAppDB(CoreApp.instance.getDB()); + } + /** * Try to execute a handler. It will schedule the next execution once done. * If the handler cannot be executed or it fails, it will be re-executed after mmCoreCronMinInterval. @@ -235,12 +246,11 @@ export class CoreCronDelegate { * @return Promise resolved with the handler's last execution time. */ protected async getHandlerLastExecutionTime(name: string): Promise { - await this.dbReady; - + const db = await this.appDB; const id = this.getHandlerLastExecutionId(name); try { - const entry = await this.appDB.getRecord(CRON_TABLE_NAME, { id }); + const entry = await db.getRecord(CRON_TABLE_NAME, { id }); const time = Number(entry.value); @@ -395,15 +405,14 @@ export class CoreCronDelegate { * @return Promise resolved when the execution time is saved. */ protected async setHandlerLastExecutionTime(name: string, time: number): Promise { - await this.dbReady; - + const db = await this.appDB; const id = this.getHandlerLastExecutionId(name); const entry = { id, value: time, }; - await this.appDB.insertRecord(CRON_TABLE_NAME, entry); + await db.insertRecord(CRON_TABLE_NAME, entry); } /** diff --git a/src/core/services/db/app.ts b/src/core/services/database/app.ts similarity index 100% rename from src/core/services/db/app.ts rename to src/core/services/database/app.ts diff --git a/src/core/services/db/config.ts b/src/core/services/database/config.ts similarity index 100% rename from src/core/services/db/config.ts rename to src/core/services/database/config.ts diff --git a/src/core/services/db/cron.ts b/src/core/services/database/cron.ts similarity index 100% rename from src/core/services/db/cron.ts rename to src/core/services/database/cron.ts diff --git a/src/core/services/db/filepool.ts b/src/core/services/database/filepool.ts similarity index 100% rename from src/core/services/db/filepool.ts rename to src/core/services/database/filepool.ts diff --git a/src/core/services/database/index.ts b/src/core/services/database/index.ts new file mode 100644 index 000000000..ab8d9bd68 --- /dev/null +++ b/src/core/services/database/index.ts @@ -0,0 +1,32 @@ +// (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 { Provider } from '@angular/core'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; + +import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './filepool'; +import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './sites'; +import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './sync'; + +export function getDatabaseProviders(): Provider[] { + return [{ + provide: CORE_SITE_SCHEMAS, + useValue: [ + FILEPOOL_SITE_SCHEMA, + SITES_SITE_SCHEMA, + SYNC_SITE_SCHEMA, + ], + multi: true, + }]; +} diff --git a/src/core/services/db/local-notifications.ts b/src/core/services/database/local-notifications.ts similarity index 100% rename from src/core/services/db/local-notifications.ts rename to src/core/services/database/local-notifications.ts diff --git a/src/core/services/db/sites.ts b/src/core/services/database/sites.ts similarity index 100% rename from src/core/services/db/sites.ts rename to src/core/services/database/sites.ts diff --git a/src/core/services/db/sync.ts b/src/core/services/database/sync.ts similarity index 100% rename from src/core/services/db/sync.ts rename to src/core/services/database/sync.ts diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 9ab08dcb2..092a3ed54 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -45,7 +45,7 @@ import { CoreFilepoolPackageEntry, CoreFilepoolQueueEntry, CoreFilepoolQueueDBEntry, -} from '@services/db/filepool'; +} from '@services/database/filepool'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -74,8 +74,6 @@ export class CoreFilepoolProvider { 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; protected logger: CoreLogger; - protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected queueState = CoreFilepoolProvider.QUEUE_PAUSED; protected urlAttributes: RegExp[] = [ new RegExp('(\\?|&)token=([A-Za-z0-9]*)'), @@ -91,17 +89,30 @@ export class CoreFilepoolProvider { protected packagesPromises: { [s: string]: { [s: string]: Promise } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; + constructor() { + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); - this.appDB = CoreApp.instance.getDB(); - this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => { - // Ignore errors. - }); - this.init(); } + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + try { + await CoreApp.instance.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + this.resolveAppDB(CoreApp.instance.getDB()); + } + /** * Init some properties. */ @@ -264,11 +275,11 @@ export class CoreFilepoolProvider { options: CoreFilepoolFileOptions = {}, link?: CoreFilepoolComponentLink, ): Promise { - await this.dbReady; - this.logger.debug(`Adding ${fileId} to the queue`); - await this.appDB.insertRecord(QUEUE_TABLE_NAME, { + const db = await this.appDB; + + await db.insertRecord(QUEUE_TABLE_NAME, { siteId, fileId, url, @@ -318,8 +329,6 @@ export class CoreFilepoolProvider { revision?: number, alreadyFixed?: boolean, ): Promise { - await this.dbReady; - if (!CoreFile.instance.isAvailable()) { throw new CoreError('File system cannot be used.'); } @@ -398,7 +407,9 @@ export class CoreFilepoolProvider { // Update only when required. this.logger.debug(`Updating file ${fileId} which is already in queue`); - return this.appDB.updateRecords(QUEUE_TABLE_NAME, newData, primaryKey).then(() => + const db = await this.appDB; + + return db.updateRecords(QUEUE_TABLE_NAME, newData, primaryKey).then(() => this.getQueuePromise(siteId, fileId, true, onProgress)); } @@ -2110,9 +2121,8 @@ export class CoreFilepoolProvider { * @return Resolved with file object from DB on success, rejected otherwise. */ protected async hasFileInQueue(siteId: string, fileId: string): Promise { - await this.dbReady; - - const entry = await this.appDB.getRecord(QUEUE_TABLE_NAME, { siteId, fileId }); + const db = await this.appDB; + const entry = await db.getRecord(QUEUE_TABLE_NAME, { siteId, fileId }); if (typeof entry === 'undefined') { throw new CoreError('File not found in queue.'); @@ -2444,12 +2454,11 @@ export class CoreFilepoolProvider { * @return Resolved on success. Rejected on failure. */ protected async processImportantQueueItem(): Promise { - await this.dbReady; - let items: CoreFilepoolQueueEntry[]; + const db = await this.appDB; try { - items = await this.appDB.getRecords( + items = await db.getRecords( QUEUE_TABLE_NAME, undefined, 'priority DESC, added ASC', @@ -2593,9 +2602,9 @@ export class CoreFilepoolProvider { * @return Resolved on success. Rejected on failure. It is advised to silently ignore failures. */ protected async removeFromQueue(siteId: string, fileId: string): Promise { - await this.dbReady; + const db = await this.appDB; - await this.appDB.deleteRecords(QUEUE_TABLE_NAME, { siteId, fileId }); + await db.deleteRecords(QUEUE_TABLE_NAME, { siteId, fileId }); } /** diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index 87de10d11..0dac0d8af 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -34,7 +34,7 @@ import { COMPONENTS_TABLE_NAME, SITES_TABLE_NAME, CodeRequestsQueueItem, -} from '@services/db/local-notifications'; +} from '@services/database/local-notifications'; /** * Service to handle local notifications. @@ -43,8 +43,6 @@ import { export class CoreLocalNotificationsProvider { protected logger: CoreLogger; - protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected codes: { [s: string]: number } = {}; protected codeRequestsQueue: {[key: string]: CodeRequestsQueueItem} = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -58,17 +56,31 @@ export class CoreLocalNotificationsProvider { protected updateSubscription?: Subscription; protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; + constructor() { + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider'); this.queueRunner = new CoreQueueRunner(10); - this.appDB = CoreApp.instance.getDB(); - this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => { - // Ignore errors. - }); this.init(); } + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + try { + await CoreApp.instance.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + this.resolveAppDB(CoreApp.instance.getDB()); + } + /** * Init some properties. */ @@ -212,8 +224,6 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the code is retrieved. */ protected async getCode(table: string, id: string): Promise { - await this.dbReady; - const key = table + '#' + id; // Check if the code is already in memory. @@ -221,23 +231,25 @@ export class CoreLocalNotificationsProvider { return this.codes[key]; } + const db = await this.appDB; + try { // Check if we already have a code stored for that ID. - const entry = await this.appDB.getRecord<{id: string; code: number}>(table, { id: id }); + const entry = await db.getRecord<{id: string; code: number}>(table, { id: id }); this.codes[key] = entry.code; return entry.code; } catch (err) { // No code stored for that ID. Create a new code for it. - const entries = await this.appDB.getRecords<{id: string; code: number}>(table, undefined, 'code DESC'); + const entries = await db.getRecords<{id: string; code: number}>(table, undefined, 'code DESC'); let newCode = 0; if (entries.length > 0) { newCode = entries[0].code + 1; } - await this.appDB.insertRecord(table, { id: id, code: newCode }); + await db.insertRecord(table, { id: id, code: newCode }); this.codes[key] = newCode; return newCode; @@ -324,10 +336,10 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved with a boolean indicating if promise is triggered (true) or not. */ async isTriggered(notification: ILocalNotification, useQueue: boolean = true): Promise { - await this.dbReady; + const db = await this.appDB; try { - const stored = await this.appDB.getRecord<{ id: number; at: number }>( + const stored = await db.getRecord<{ id: number; at: number }>( TRIGGERED_TABLE_NAME, { id: notification.id }, ); @@ -481,9 +493,9 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when it is removed. */ async removeTriggered(id: number): Promise { - await this.dbReady; + const db = await this.appDB; - await this.appDB.deleteRecords(TRIGGERED_TABLE_NAME, { id: id }); + await db.deleteRecords(TRIGGERED_TABLE_NAME, { id: id }); } /** @@ -639,14 +651,13 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when stored, rejected otherwise. */ async trigger(notification: ILocalNotification): Promise { - await this.dbReady; - + const db = await this.appDB; const entry = { id: notification.id, at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(), }; - return this.appDB.insertRecord(TRIGGERED_TABLE_NAME, entry); + return db.insertRecord(TRIGGERED_TABLE_NAME, entry); } /** @@ -657,12 +668,12 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when done. */ async updateComponentName(oldName: string, newName: string): Promise { - await this.dbReady; + const db = await this.appDB; const oldId = COMPONENTS_TABLE_NAME + '#' + oldName; const newId = COMPONENTS_TABLE_NAME + '#' + newName; - await this.appDB.updateRecords(COMPONENTS_TABLE_NAME, { id: newId }, { id: oldId }); + await db.updateRecords(COMPONENTS_TABLE_NAME, { id: newId }, { id: oldId }); } } diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index e635d681d..513c11f9c 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -47,7 +47,7 @@ import { SiteDBEntry, CurrentSiteDBEntry, SchemaVersionsDBEntry, -} from '@services/db/sites'; +} from '@services/database/sites'; import { CoreArray } from '../singletons/array'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); @@ -61,25 +61,27 @@ export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export class CoreSitesProvider { // Constants to validate a site version. - protected readonly WORKPLACE_APP = 3; - protected readonly MOODLE_APP = 2; - protected readonly VALID_VERSION = 1; - protected readonly INVALID_VERSION = -1; + protected static readonly WORKPLACE_APP = 3; + protected static readonly MOODLE_APP = 2; + protected static readonly VALID_VERSION = 1; + protected static readonly INVALID_VERSION = -1; protected isWPApp = false; - protected logger: CoreLogger; protected services = {}; protected sessionRestored = false; protected currentSite?: CoreSite; protected sites: { [s: string]: CoreSite } = {}; - protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected siteSchemasMigration: { [siteId: string]: Promise } = {}; protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; + constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) { + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreSitesProvider'); this.siteSchemas = CoreArray.flatten(siteSchemas).reduce( (siteSchemas, schema) => { @@ -89,10 +91,19 @@ export class CoreSitesProvider { }, this.siteSchemas, ); - this.appDB = CoreApp.instance.getDB(); - this.dbReady = CoreApp.instance.createTablesFromSchema(APP_SCHEMA).catch(() => { + } + + /** + * Initialise database. + */ + async initialiseDatabase(): Promise { + try { + await CoreApp.instance.createTablesFromSchema(APP_SCHEMA); + } catch (e) { // Ignore errors. - }); + } + + this.resolveAppDB(CoreApp.instance.getDB()); } /** @@ -429,7 +440,7 @@ export class CoreSitesProvider { const info = await candidateSite.fetchSiteInfo(); const result = this.isValidMoodleVersion(info); - if (result != this.VALID_VERSION) { + if (result != CoreSitesProvider.VALID_VERSION) { return this.treatInvalidAppVersion(result, siteUrl); } @@ -491,7 +502,7 @@ export class CoreSitesProvider { } catch (error) { // Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp. if (error && error.errorcode == 'invaliddevice') { - return this.treatInvalidAppVersion(this.WORKPLACE_APP, siteUrl); + return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP, siteUrl); } throw error; @@ -512,11 +523,11 @@ export class CoreSitesProvider { let translateParams; switch (result) { - case this.MOODLE_APP: + case CoreSitesProvider.MOODLE_APP: errorKey = 'core.login.connecttomoodleapp'; errorCode = 'connecttomoodleapp'; break; - case this.WORKPLACE_APP: + case CoreSitesProvider.WORKPLACE_APP: errorKey = 'core.login.connecttoworkplaceapp'; errorCode = 'connecttoworkplaceapp'; break; @@ -581,7 +592,7 @@ export class CoreSitesProvider { */ protected isValidMoodleVersion(info: CoreSiteInfoResponse): number { if (!info) { - return this.INVALID_VERSION; + return CoreSitesProvider.INVALID_VERSION; } const version31 = 2016052300; @@ -606,7 +617,7 @@ export class CoreSitesProvider { } // Couldn't validate it. - return this.INVALID_VERSION; + return CoreSitesProvider.INVALID_VERSION; } /** @@ -623,14 +634,14 @@ export class CoreSitesProvider { } if (!this.isWPApp && isWorkplace) { - return this.WORKPLACE_APP; + return CoreSitesProvider.WORKPLACE_APP; } if (this.isWPApp && !isWorkplace) { - return this.MOODLE_APP; + return CoreSitesProvider.MOODLE_APP; } - return this.VALID_VERSION; + return CoreSitesProvider.VALID_VERSION; } /** @@ -669,8 +680,7 @@ export class CoreSitesProvider { config?: CoreSiteConfig, oauthId?: number, ): Promise { - await this.dbReady; - + const db = await this.appDB; const entry = { id, siteUrl, @@ -682,7 +692,7 @@ export class CoreSitesProvider { oauthId, }; - await this.appDB.insertRecord(SITES_TABLE_NAME, entry); + await db.insertRecord(SITES_TABLE_NAME, entry); } /** @@ -893,8 +903,6 @@ export class CoreSitesProvider { * @return Promise to be resolved when the site is deleted. */ async deleteSite(siteId: string): Promise { - await this.dbReady; - this.logger.debug(`Delete site ${siteId}`); if (typeof this.currentSite != 'undefined' && this.currentSite.id == siteId) { @@ -909,7 +917,9 @@ export class CoreSitesProvider { delete this.sites[siteId]; try { - await this.appDB.deleteRecords(SITES_TABLE_NAME, { id: siteId }); + const db = await this.appDB; + + await db.deleteRecords(SITES_TABLE_NAME, { id: siteId }); } catch (err) { // DB remove shouldn't fail, but we'll go ahead even if it does. } @@ -926,9 +936,8 @@ export class CoreSitesProvider { * @return Promise resolved with true if there are sites and false if there aren't. */ async hasSites(): Promise { - await this.dbReady; - - const count = await this.appDB.countRecords(SITES_TABLE_NAME); + const db = await this.appDB; + const count = await db.countRecords(SITES_TABLE_NAME); return count > 0; } @@ -940,8 +949,6 @@ export class CoreSitesProvider { * @return Promise resolved with the site. */ async getSite(siteId?: string): Promise { - await this.dbReady; - if (!siteId) { if (this.currentSite) { return this.currentSite; @@ -954,7 +961,8 @@ export class CoreSitesProvider { return this.sites[siteId]; } else { // Retrieve and create the site. - const data = await this.appDB.getRecord(SITES_TABLE_NAME, { id: siteId }); + const db = await this.appDB; + const data = await db.getRecord(SITES_TABLE_NAME, { id: siteId }); return this.makeSiteFromSiteListEntry(data); } @@ -1025,9 +1033,8 @@ export class CoreSitesProvider { * @return Promise resolved when the sites are retrieved. */ async getSites(ids?: string[]): Promise { - await this.dbReady; - - const sites = await this.appDB.getAllRecords(SITES_TABLE_NAME); + const db = await this.appDB; + const sites = await db.getAllRecords(SITES_TABLE_NAME); const formattedSites: CoreSiteBasicInfo[] = []; sites.forEach((site) => { @@ -1089,9 +1096,8 @@ export class CoreSitesProvider { * @return Promise resolved when the sites IDs are retrieved. */ async getLoggedInSitesIds(): Promise { - await this.dbReady; - - const sites = await this.appDB.getRecords(SITES_TABLE_NAME, { loggedOut : 0 }); + const db = await this.appDB; + const sites = await db.getRecords(SITES_TABLE_NAME, { loggedOut : 0 }); return sites.map((site) => site.id); } @@ -1102,9 +1108,8 @@ export class CoreSitesProvider { * @return Promise resolved when the sites IDs are retrieved. */ async getSitesIds(): Promise { - await this.dbReady; - - const sites = await this.appDB.getAllRecords(SITES_TABLE_NAME); + const db = await this.appDB; + const sites = await db.getAllRecords(SITES_TABLE_NAME); return sites.map((site) => site.id); } @@ -1116,14 +1121,13 @@ export class CoreSitesProvider { * @return Promise resolved when current site is stored. */ async login(siteId: string): Promise { - await this.dbReady; - + const db = await this.appDB; const entry = { id: 1, siteId, }; - await this.appDB.insertRecord(CURRENT_SITE_TABLE_NAME, entry); + await db.insertRecord(CURRENT_SITE_TABLE_NAME, entry); CoreEvents.trigger(CoreEvents.LOGIN, {}, siteId); } @@ -1134,12 +1138,11 @@ export class CoreSitesProvider { * @return Promise resolved when the user is logged out. */ async logout(): Promise { - await this.dbReady; - let siteId: string | undefined; const promises: Promise[] = []; if (this.currentSite) { + const db = await this.appDB; const siteConfig = this.currentSite.getStoredConfig(); siteId = this.currentSite.getId(); @@ -1149,7 +1152,7 @@ export class CoreSitesProvider { promises.push(this.setSiteLoggedOut(siteId, true)); } - promises.push(this.appDB.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 })); + promises.push(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 })); } try { @@ -1169,12 +1172,12 @@ export class CoreSitesProvider { return Promise.reject(new CoreError('Session already restored.')); } - await this.dbReady; + const db = await this.appDB; this.sessionRestored = true; try { - const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); + const currentSite = await db.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); const siteId = currentSite.siteId; this.logger.debug(`Restore session in site ${siteId}`); @@ -1192,8 +1195,7 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { - await this.dbReady; - + const db = await this.appDB; const site = await this.getSite(siteId); const newValues = { token: '', // Erase the token for security. @@ -1202,7 +1204,7 @@ export class CoreSitesProvider { site.setLoggedOut(loggedOut); - await this.appDB.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); } /** @@ -1238,8 +1240,7 @@ export class CoreSitesProvider { * @return A promise resolved when the site is updated. */ async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { - await this.dbReady; - + const db = await this.appDB; const site = await this.getSite(siteId); const newValues = { token, @@ -1251,7 +1252,7 @@ export class CoreSitesProvider { site.privateToken = privateToken; site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. - await this.appDB.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); } /** @@ -1261,8 +1262,6 @@ export class CoreSitesProvider { * @return A promise resolved when the site is updated. */ async updateSiteInfo(siteId: string): Promise { - await this.dbReady; - const site = await this.getSite(siteId); try { @@ -1270,7 +1269,7 @@ export class CoreSitesProvider { site.setInfo(info); const versionCheck = this.isValidMoodleVersion(info); - if (versionCheck != this.VALID_VERSION) { + if (versionCheck != CoreSitesProvider.VALID_VERSION) { // The Moodle version is not supported, reject. return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId()); } @@ -1295,7 +1294,9 @@ export class CoreSitesProvider { } try { - await this.appDB.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + const db = await this.appDB; + + await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); } finally { CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId); } @@ -1328,8 +1329,6 @@ export class CoreSitesProvider { * @return Promise resolved with the site IDs (array). */ async getSiteIdsFromUrl(url: string, prioritize?: boolean, username?: string): Promise { - await this.dbReady; - // If prioritize is true, check current site first. if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) { if (!username || this.currentSite?.getInfo()?.username == username) { @@ -1354,7 +1353,8 @@ export class CoreSitesProvider { } try { - const siteEntries = await this.appDB.getAllRecords(SITES_TABLE_NAME); + const db = await this.appDB; + const siteEntries = await db.getAllRecords(SITES_TABLE_NAME); const ids: string[] = []; const promises: Promise[] = []; @@ -1385,9 +1385,8 @@ export class CoreSitesProvider { * @return Promise resolved with the site ID. */ async getStoredCurrentSiteId(): Promise { - await this.dbReady; - - const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); + const db = await this.appDB; + const currentSite = await db.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); return currentSite.siteId; } diff --git a/src/core/services/sync.ts b/src/core/services/sync.ts index b650e2998..7c3c7ca22 100644 --- a/src/core/services/sync.ts +++ b/src/core/services/sync.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { makeSingleton } from '@singletons'; -import { SYNC_TABLE_NAME, CoreSyncRecord } from '@services/db/sync'; +import { SYNC_TABLE_NAME, CoreSyncRecord } from '@services/database/sync'; /* * Service that provides some features regarding synchronization. From e181a019e5be96dc412f8c36ac02b175337a1e0e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 7 Dec 2020 14:31:40 +0100 Subject: [PATCH 4/4] MOBILE-3320 core: Refactor singletons definition --- src/core/classes/singletons-factory.ts | 88 -------------------------- src/core/singletons/index.ts | 49 ++++++++++++-- src/testing/utils.ts | 2 +- 3 files changed, 43 insertions(+), 96 deletions(-) delete mode 100644 src/core/classes/singletons-factory.ts diff --git a/src/core/classes/singletons-factory.ts b/src/core/classes/singletons-factory.ts deleted file mode 100644 index d574f6591..000000000 --- a/src/core/classes/singletons-factory.ts +++ /dev/null @@ -1,88 +0,0 @@ -// (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 { Injector, Type } from '@angular/core'; - -/** - * Stub class used to type anonymous classes created in CoreSingletonsFactory#makeSingleton method. - */ -class CoreSingleton {} - -/** - * Token that can be used to resolve instances from the injector. - */ -export type CoreInjectionToken = Type | Type | string; - -/** - * Singleton class created using the factory. - */ -export type CoreSingletonClass = typeof CoreSingleton & { - instance: Service; - setInstance(instance: Service): void; -}; - -/** - * Factory used to create CoreSingleton classes that get instances from an injector. - */ -export class CoreSingletonsFactory { - - /** - * Angular injector used to resolve singleton instances. - */ - private injector?: Injector; - - /** - * Set the injector that will be used to resolve instances in the singletons created with this factory. - * - * @param injector Injector. - */ - setInjector(injector: Injector): void { - this.injector = injector; - } - - /** - * Make a singleton that will hold an instance resolved from the factory injector. - * - * @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the - * provider was defined using a class or the string used in the `provide` key if it was defined using an object. - */ - makeSingleton(injectionToken: CoreInjectionToken): CoreSingletonClass { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const factory = this; - - return class { - - private static serviceInstance: Service; - - static get instance(): Service { - // Initialize instances lazily. - if (!this.serviceInstance) { - if (!factory.injector) { - throw new Error('Can\'t resolve a singleton instance without an injector'); - } - - this.serviceInstance = factory.injector.get(injectionToken); - } - - return this.serviceInstance; - } - - static setInstance(instance: Service): void { - this.serviceInstance = instance; - } - - }; - } - -} diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 161f310ae..bab681879 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ApplicationRef, ApplicationInitStatus, Injector, NgZone as NgZoneService } from '@angular/core'; +import { ApplicationRef, ApplicationInitStatus, Injector, NgZone as NgZoneService, Type } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { @@ -52,9 +52,23 @@ import { Zip as ZipService } from '@ionic-native/zip/ngx'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory'; +/** + * Injector instance used to resolve singletons. + */ +let singletonsInjector: Injector | null = null; -const factory = new CoreSingletonsFactory(); +/** + * Stub class used to type anonymous classes created in the makeSingleton method. + */ +class CoreSingleton {} + +/** + * Singleton class created using the factory. + */ +export type CoreSingletonClass = typeof CoreSingleton & { + instance: Service; + setInstance(instance: Service): void; +}; /** * Set the injector that will be used to resolve instances in the singletons of this module. @@ -62,17 +76,38 @@ const factory = new CoreSingletonsFactory(); * @param injector Module injector. */ export function setSingletonsInjector(injector: Injector): void { - factory.setInjector(injector); + singletonsInjector = injector; } /** - * Make a singleton for this module. + * Make a singleton for the given injection token. * * @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the * provider was defined using a class or the string used in the `provide` key if it was defined using an object. */ -export function makeSingleton(injectionToken: CoreInjectionToken): CoreSingletonClass { - return factory.makeSingleton(injectionToken); +export function makeSingleton(injectionToken: Type | Type | string): CoreSingletonClass { + return class { + + private static serviceInstance: Service; + + static get instance(): Service { + // Initialize instances lazily. + if (!this.serviceInstance) { + if (!singletonsInjector) { + throw new Error('Can\'t resolve a singleton instance without an injector'); + } + + this.serviceInstance = singletonsInjector.get(injectionToken); + } + + return this.serviceInstance; + } + + static setInstance(instance: Service): void { + this.serviceInstance = instance; + } + + }; } // Convert ionic-native services to singleton. diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 97d6bba4b..8bbfddc4e 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -15,7 +15,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, Type, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CoreSingletonClass } from '@classes/singletons-factory'; +import { CoreSingletonClass } from '@singletons'; abstract class WrapperComponent {