MOBILE-4272 workshop: Decouple handlers

main
Noel De Martin 2023-07-26 11:45:14 +09:00
parent a6eeacfd88
commit a5cd697012
20 changed files with 635 additions and 482 deletions

View File

@ -38,6 +38,7 @@ import {
} from '../../services/workshop'; } from '../../services/workshop';
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper'; import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Component that displays workshop assessment strategy form. * Component that displays workshop assessment strategy form.
@ -75,7 +76,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
feedbackControl = new FormControl(); feedbackControl = new FormControl();
overallFeedkback = false; overallFeedkback = false;
overallFeedkbackRequired = false; overallFeedkbackRequired = false;
component = AddonModWorkshopProvider.COMPONENT; component = ADDON_MOD_WORKSHOP_COMPONENT;
componentId?: number; componentId?: number;
weights: number[] = []; weights: number[] = [];
weight?: number; weight?: number;
@ -128,7 +129,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
// Check if rich text editor is enabled. // Check if rich text editor is enabled.
if (this.edit) { if (this.edit) {
// Block the workshop. // Block the workshop.
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id); CoreSync.blockOperation(ADDON_MOD_WORKSHOP_COMPONENT, this.workshop.id);
} }
try { try {
@ -232,7 +233,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
this.originalData.selectedValues = CoreUtils.clone(this.data.selectedValues); this.originalData.selectedValues = CoreUtils.clone(this.data.selectedValues);
if (this.edit) { if (this.edit) {
CoreFileSession.setFiles( CoreFileSession.setFiles(
AddonModWorkshopProvider.COMPONENT, ADDON_MOD_WORKSHOP_COMPONENT,
this.workshop.id + '_' + this.assessmentId, this.workshop.id + '_' + this.assessmentId,
this.data.assessment.feedbackattachmentfiles, this.data.assessment.feedbackattachmentfiles,
); );
@ -265,7 +266,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
// Compare feedback files. // Compare feedback files.
const files = CoreFileSession.getFiles( const files = CoreFileSession.getFiles(
AddonModWorkshopProvider.COMPONENT, ADDON_MOD_WORKSHOP_COMPONENT,
this.workshop.id + '_' + this.assessmentId, this.workshop.id + '_' + this.assessmentId,
) || []; ) || [];
if (CoreFileUploader.areFileListDifferent(files, this.originalData.files)) { if (CoreFileUploader.areFileListDifferent(files, this.originalData.files)) {
@ -290,7 +291,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
} }
const files = CoreFileSession.getFiles( const files = CoreFileSession.getFiles(
AddonModWorkshopProvider.COMPONENT, ADDON_MOD_WORKSHOP_COMPONENT,
this.workshop.id + '_' + this.assessmentId, this.workshop.id + '_' + this.assessmentId,
) || []; ) || [];

View File

@ -25,7 +25,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
import { import {
AddonModWorkshopProvider, AddonModWorkshopProvider,
AddonModWorkshopPhase, AddonModWorkshopPhase,
@ -53,6 +52,7 @@ import {
AddonModWorkshopSyncResult, AddonModWorkshopSyncResult,
} from '../../services/workshop-sync'; } from '../../services/workshop-sync';
import { AddonModWorkshopPhaseInfoComponent } from '../phase/phase'; import { AddonModWorkshopPhaseInfoComponent } from '../phase/phase';
import { ADDON_MOD_WORKSHOP_COMPONENT, ADDON_MOD_WORKSHOP_PAGE_NAME } from '@addons/mod/workshop/constants';
/** /**
* Component that displays a workshop index page. * Component that displays a workshop index page.
@ -65,7 +65,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
@Input() group = 0; @Input() group = 0;
component = AddonModWorkshopProvider.COMPONENT; component = ADDON_MOD_WORKSHOP_COMPONENT;
pluginName = 'workshop'; pluginName = 'workshop';
workshop?: AddonModWorkshopData; workshop?: AddonModWorkshopData;
@ -379,7 +379,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
const submissionId = this.submission?.id || 0; const submissionId = this.submission?.id || 0;
CoreNavigator.navigateToSitePath( CoreNavigator.navigateToSitePath(
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${submissionId}/edit`, `${ADDON_MOD_WORKSHOP_PAGE_NAME}/${this.courseId}/${this.module.id}/${submissionId}/edit`,
{ params }, { params },
); );

View File

@ -19,9 +19,7 @@ import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { AddonModWorkshopSubmissionPage } from '../../pages/submission/submission'; import { AddonModWorkshopSubmissionPage } from '../../pages/submission/submission';
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
import { import {
AddonModWorkshopProvider,
AddonModWorkshopPhase, AddonModWorkshopPhase,
AddonModWorkshopData, AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse, AddonModWorkshopGetWorkshopAccessInformationWSResponse,
@ -32,6 +30,7 @@ import {
AddonModWorkshopSubmissionDataWithOfflineData, AddonModWorkshopSubmissionDataWithOfflineData,
} from '../../services/workshop-helper'; } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { ADDON_MOD_WORKSHOP_COMPONENT, ADDON_MOD_WORKSHOP_PAGE_NAME } from '@addons/mod/workshop/constants';
/** /**
* Component that displays workshop submission. * Component that displays workshop submission.
@ -51,7 +50,7 @@ export class AddonModWorkshopSubmissionComponent implements OnInit {
@Input() assessment?: AddonModWorkshopSubmissionAssessmentWithFormData; @Input() assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
@Input() summary = false; @Input() summary = false;
component = AddonModWorkshopProvider.COMPONENT; component = ADDON_MOD_WORKSHOP_COMPONENT;
componentId?: number; componentId?: number;
userId: number; userId: number;
loaded = false; loaded = false;
@ -128,7 +127,7 @@ export class AddonModWorkshopSubmissionComponent implements OnInit {
}; };
CoreNavigator.navigateToSitePath( CoreNavigator.navigateToSitePath(
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${this.submission.id}`, `${ADDON_MOD_WORKSHOP_PAGE_NAME}/${this.courseId}/${this.module.id}/${this.submission.id}`,
{ params }, { params },
); );

View File

@ -0,0 +1,41 @@
// (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.
export const ADDON_MOD_WORKSHOP_COMPONENT = 'mmaModWorkshop';
// Routing.
export const ADDON_MOD_WORKSHOP_PAGE_NAME = 'mod_workshop';
// Handlers.
export const ADDON_MOD_WORKSHOP_PREFETCH_NAME = 'AddonModWorkshop';
export const ADDON_MOD_WORKSHOP_PREFETCH_MODNAME = 'workshop';
export const ADDON_MOD_WORKSHOP_PREFETCH_COMPONENT = ADDON_MOD_WORKSHOP_COMPONENT;
export const ADDON_MOD_WORKSHOP_PREFETCH_UPDATE_NAMES = new RegExp(
[
'^configuration$',
'^.*files$',
'^completion',
'^gradeitems$',
'^outcomes$',
'^submissions$',
'^assessments$' +
'^assessmentgrades$',
'^usersubmissions$',
'^userassessments$',
'^userassessmentgrades$',
'^userassessmentgrades$',
].join('|'),
);
export const ADDON_MOD_WORKSHOP_SYNC_CRON_NAME = 'AddonModWorkshopSyncCronHandler';

View File

@ -41,6 +41,7 @@ import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync'; import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync';
import { CoreTime } from '@singletons/time'; import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Page that displays a workshop assessment. * Page that displays a workshop assessment.
@ -198,7 +199,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea
if (this.assessmentId && (this.access.canallocate || this.access.canoverridegrades)) { if (this.assessmentId && (this.access.canallocate || this.access.canoverridegrades)) {
if (!this.isDestroyed) { if (!this.isDestroyed) {
// Block the workshop. // Block the workshop.
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); CoreSync.blockOperation(ADDON_MOD_WORKSHOP_COMPONENT, this.workshopId);
} }
this.evaluating = true; this.evaluating = true;
@ -413,7 +414,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea
this.syncObserver?.off(); this.syncObserver?.off();
// Restore original back functions. // Restore original back functions.
CoreSync.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId); CoreSync.unblockOperation(ADDON_MOD_WORKSHOP_COMPONENT, this.workshopId);
} }
} }

View File

@ -41,6 +41,7 @@ import {
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper'; import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper';
import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Page that displays the workshop edit submission. * Page that displays the workshop edit submission.
@ -59,7 +60,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca
submission?: AddonModWorkshopSubmissionDataWithOfflineData; submission?: AddonModWorkshopSubmissionDataWithOfflineData;
loaded = false; loaded = false;
component = AddonModWorkshopProvider.COMPONENT; component = ADDON_MOD_WORKSHOP_COMPONENT;
componentId!: number; componentId!: number;
editForm: FormGroup; // The form group. editForm: FormGroup; // The form group.
editorExtraParams: Record<string, unknown> = {}; // Extra params to identify the draft. editorExtraParams: Record<string, unknown> = {}; // Extra params to identify the draft.

View File

@ -49,6 +49,7 @@ import { AddonModWorkshopOffline } from '../../services/workshop-offline';
import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync'; import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { CoreTime } from '@singletons/time'; import { CoreTime } from '@singletons/time';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Page that displays a workshop submission. * Page that displays a workshop submission.
@ -99,7 +100,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
}; };
protected hasOffline = false; protected hasOffline = false;
protected component = AddonModWorkshopProvider.COMPONENT; protected component = ADDON_MOD_WORKSHOP_COMPONENT;
protected forceLeave = false; protected forceLeave = false;
protected obsAssessmentSaved: CoreEventObserver; protected obsAssessmentSaved: CoreEventObserver;
protected syncObserver: CoreEventObserver; protected syncObserver: CoreEventObserver;

View File

@ -15,7 +15,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { AddonModWorkshopProvider } from '../workshop'; import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Handler to treat links to workshop. * Handler to treat links to workshop.
*/ */
@ -25,7 +26,7 @@ export class AddonModWorkshopIndexLinkHandlerService extends CoreContentLinksMod
name = 'AddonModWorkshopLinkHandler'; name = 'AddonModWorkshopLinkHandler';
constructor() { constructor() {
super(AddonModWorkshopProvider.COMPONENT, 'workshop', 'w'); super(ADDON_MOD_WORKSHOP_COMPONENT, 'workshop', 'w');
} }
} }

View File

@ -13,11 +13,11 @@
// limitations under the License. // limitations under the License.
import { CoreConstants, ModPurpose } from '@/core/constants'; import { CoreConstants, ModPurpose } from '@/core/constants';
import { ADDON_MOD_WORKSHOP_PAGE_NAME } from '@addons/mod/workshop/constants';
import { Injectable, Type } from '@angular/core'; import { Injectable, Type } from '@angular/core';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourseModuleHandler } from '@features/course/services/module-delegate'; import { CoreCourseModuleHandler } from '@features/course/services/module-delegate';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { AddonModWorkshopIndexComponent } from '../../components/index';
/** /**
* Handler to support workshop modules. * Handler to support workshop modules.
@ -25,11 +25,9 @@ import { AddonModWorkshopIndexComponent } from '../../components/index';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AddonModWorkshopModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler { export class AddonModWorkshopModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_workshop';
name = 'AddonModWorkshop'; name = 'AddonModWorkshop';
modName = 'workshop'; modName = 'workshop';
protected pageName = AddonModWorkshopModuleHandlerService.PAGE_NAME; protected pageName = ADDON_MOD_WORKSHOP_PAGE_NAME;
supportedFeatures = { supportedFeatures = {
[CoreConstants.FEATURE_GROUPS]: true, [CoreConstants.FEATURE_GROUPS]: true,
@ -47,6 +45,8 @@ export class AddonModWorkshopModuleHandlerService extends CoreModuleHandlerBase
* @inheritdoc * @inheritdoc
*/ */
async getMainComponent(): Promise<Type<unknown>> { async getMainComponent(): Promise<Type<unknown>> {
const { AddonModWorkshopIndexComponent } = await import('@addons/mod/workshop/components/index');
return AddonModWorkshopIndexComponent; return AddonModWorkshopIndexComponent;
} }

View File

@ -0,0 +1,405 @@
// (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 { AddonModDataSyncResult } from '@addons/mod/data/services/data-sync';
import { Injectable } from '@angular/core';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreUser } from '@features/user/services/user';
import { CoreFilepool } from '@services/filepool';
import { CoreGroup, CoreGroups } from '@services/groups';
import { CoreSites, CoreSitesReadingStrategy, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import {
AddonModWorkshop,
AddonModWorkshopPhase,
AddonModWorkshopGradesData,
AddonModWorkshopData,
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
} from '../workshop';
import { AddonModWorkshopHelper } from '../workshop-helper';
import { AddonModWorkshopSync } from '../workshop-sync';
import { AddonModWorkshopPrefetchHandlerService } from '@addons/mod/workshop/services/handlers/prefetch';
/**
* Handler to prefetch workshops.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopPrefetchHandlerLazyService extends AddonModWorkshopPrefetchHandlerService {
/**
* @inheritdoc
*/
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSFile[]> {
const info = await this.getWorkshopInfoHelper(module, courseId, { omitFail: true });
return info.files;
}
/**
* Helper function to get all workshop info just once.
*
* @param module Module to get the files.
* @param courseId Course ID the module belongs to.
* @param options Other options.
* @returns Promise resolved with the info fetched.
*/
protected async getWorkshopInfoHelper(
module: CoreCourseAnyModuleData,
courseId: number,
options: AddonModWorkshopGetInfoOptions = {},
): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
let groups: CoreGroup[] = [];
let files: CoreWSFile[] = [];
let workshop: AddonModWorkshopData;
let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
const modOptions = {
cmId: module.id,
...options, // Include all options.
};
const site = await CoreSites.getSite(options.siteId);
options.siteId = options.siteId ?? site.getId();
const userId = site.getUserId();
try {
workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
} catch (error) {
if (options.omitFail) {
// Any error, return the info we have.
return {
groups: [],
files: [],
};
}
throw error;
}
try {
files = this.getIntroFilesFromInstance(module, workshop);
files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
access = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions);
if (access.canviewallsubmissions) {
const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId);
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{ id: 0, name: '' }];
}
groups = groupInfo.groups;
}
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
// Get submission phase info.
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
const promises: Promise<void>[] = [];
if (canSubmit) {
promises.push(AddonModWorkshopHelper.getUserSubmission(workshop.id, {
userId,
cmId: module.id,
}).then((submission) => {
if (submission) {
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
}
return;
}));
}
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
promises.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions).then(async (submissions) => {
await Promise.all(submissions.map(async (submission) => {
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop.id, submission.id, {
cmId: module.id,
});
assessments.forEach((assessment) => {
files = files.concat(assessment.feedbackattachmentfiles)
.concat(assessment.feedbackcontentfiles);
});
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
await Promise.all(assessments.map((assessment) =>
AddonModWorkshopHelper.getReviewerAssessmentById(workshop.id, assessment.id)));
}
}));
return;
}));
}
// Get assessment files.
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
promises.push(AddonModWorkshopHelper.getReviewerAssessments(workshop.id, modOptions).then((assessments) => {
assessments.forEach((assessment) => {
files = files.concat(<CoreWSExternalFile[]>assessment.feedbackattachmentfiles)
.concat(assessment.feedbackcontentfiles);
});
return;
}));
}
await Promise.all(promises);
return {
workshop,
groups,
files: files.filter((file) => file !== undefined),
};
} catch (error) {
if (options.omitFail) {
// Any error, return the info we have.
return {
workshop,
groups,
files: files.filter((file) => file !== undefined),
};
}
throw error;
}
}
/**
* @inheritdoc
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModWorkshop.invalidateContent(moduleId, courseId);
}
/**
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @returns Whether the module can be downloaded. The promise should never be rejected.
*/
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, {
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
});
const accessData = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, { cmId: module.id });
// Check if workshop is setup by phase.
return accessData.canswitchphase || workshop.phase > AddonModWorkshopPhase.PHASE_SETUP;
}
/**
* @inheritdoc
*/
prefetch(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
return this.prefetchPackage(module, courseId, (siteId) => this.prefetchWorkshop(module, courseId, siteId));
}
/**
* Retrieves all the grades reports for all the groups and then returns only unique grades.
*
* @param workshopId Workshop ID.
* @param groups Array of groups in the activity.
* @param cmId Module ID.
* @param siteId Site ID. If not defined, current site.
* @returns All unique entries.
*/
protected async getAllGradesReport(
workshopId: number,
groups: CoreGroup[],
cmId: number,
siteId: string,
): Promise<AddonModWorkshopGradesData[]> {
const promises: Promise<AddonModWorkshopGradesData[]>[] = [];
groups.forEach((group) => {
promises.push(AddonModWorkshop.fetchAllGradeReports(workshopId, { groupId: group.id, cmId, siteId }));
});
const grades = await Promise.all(promises);
const uniqueGrades: Record<number, AddonModWorkshopGradesData> = {};
grades.forEach((groupGrades) => {
groupGrades.forEach((grade) => {
if (grade.submissionid) {
uniqueGrades[grade.submissionid] = grade;
}
});
});
return CoreUtils.objectToArray(uniqueGrades);
}
/**
* Prefetch a workshop.
*
* @param module The module object returned by WS.
* @param courseId Course ID the module belongs to.
* @param siteId Site ID.
* @returns Promise resolved when done.
*/
protected async prefetchWorkshop(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise<void> {
const userIds: number[] = [];
const commonOptions = {
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
siteId,
};
const modOptions = {
cmId: module.id,
...commonOptions, // Include all common options.
};
const site = await CoreSites.getSite(siteId);
const currentUserId = site.getUserId();
// Prefetch the workshop data.
const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
if (!info.workshop) {
// It would throw an exception so it would not happen.
return;
}
const workshop = info.workshop;
const promises: Promise<unknown>[] = [];
const assessmentIds: number[] = [];
promises.push(CoreFilepool.addFilesToQueue(siteId, info.files, this.component, module.id));
promises.push(AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions).then(async (access) => {
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
// Get submission phase info.
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
const promises2: Promise<unknown>[] = [];
if (canSubmit) {
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
// Add userId to the profiles to prefetch.
userIds.push(currentUserId);
}
let reportPromise: Promise<unknown> = Promise.resolve();
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
// eslint-disable-next-line promise/no-nesting
reportPromise = this.getAllGradesReport(workshop.id, info.groups, module.id, siteId).then((grades) => {
grades.forEach((grade) => {
userIds.push(grade.userid);
grade.submissiongradeoverby && userIds.push(grade.submissiongradeoverby);
grade.reviewedby && grade.reviewedby.forEach((assessment) => {
userIds.push(assessment.userid);
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
});
grade.reviewerof && grade.reviewerof.forEach((assessment) => {
userIds.push(assessment.userid);
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
});
});
return;
});
}
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
// Wait the report promise to finish to override assessments array if needed.
reportPromise = reportPromise.finally(async () => {
const revAssessments = await AddonModWorkshopHelper.getReviewerAssessments(workshop.id, {
userId: currentUserId,
cmId: module.id,
siteId,
});
let files: CoreWSExternalFile[] = []; // Files in each submission.
revAssessments.forEach((assessment) => {
if (assessment.submission?.authorid == currentUserId) {
promises.push(AddonModWorkshop.getAssessment(
workshop.id,
assessment.id,
modOptions,
));
}
userIds.push(assessment.reviewerid);
userIds.push(assessment.gradinggradeoverby);
assessmentIds[assessment.id] = assessment.id;
files = files.concat(assessment.submission?.attachmentfiles || [])
.concat(assessment.submission?.contentfiles || []);
});
await CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id);
});
}
reportPromise = reportPromise.finally(() => {
if (assessmentIds.length > 0) {
return Promise.all(assessmentIds.map((assessmentId) =>
AddonModWorkshop.getAssessmentForm(workshop.id, assessmentId, modOptions)));
}
});
promises2.push(reportPromise);
if (workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
promises2.push(AddonModWorkshop.getGrades(workshop.id, modOptions));
if (access.canviewpublishedsubmissions) {
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
}
}
await Promise.all(promises2);
return;
}));
// Add Basic Info to manage links.
promises.push(CoreCourse.getModuleBasicInfoByInstance(workshop.id, 'workshop', { siteId }));
promises.push(CoreCourse.getModuleBasicGradeInfo(module.id, siteId));
// Get course data, needed to determine upload max size if it's configured to be course limit.
promises.push(CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', courseId, siteId)));
await Promise.all(promises);
// Prefetch user profiles.
await CoreUser.prefetchProfiles(userIds, courseId, siteId);
}
/**
* @inheritdoc
*/
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModDataSyncResult> {
return AddonModWorkshopSync.syncWorkshop(module.instance, siteId);
}
}
export const AddonModWorkshopPrefetchHandler = makeSingleton(AddonModWorkshopPrefetchHandlerLazyService);
/**
* Options to pass to getWorkshopInfoHelper.
*/
export type AddonModWorkshopGetInfoOptions = CoreSitesCommonWSOptions & {
omitFail?: boolean; // True to always return even if fails.
};

View File

@ -12,401 +12,38 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { AddonModDataSyncResult } from '@addons/mod/data/services/data-sync'; import { asyncInstance } from '@/core/utils/async-instance';
import { Injectable } from '@angular/core';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreUser } from '@features/user/services/user';
import { CoreFilepool } from '@services/filepool';
import { CoreGroup, CoreGroups } from '@services/groups';
import { CoreSites, CoreSitesReadingStrategy, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { import {
AddonModWorkshopProvider, ADDON_MOD_WORKSHOP_PREFETCH_COMPONENT,
AddonModWorkshop, ADDON_MOD_WORKSHOP_PREFETCH_MODNAME,
AddonModWorkshopPhase, ADDON_MOD_WORKSHOP_PREFETCH_NAME,
AddonModWorkshopGradesData, ADDON_MOD_WORKSHOP_PREFETCH_UPDATE_NAMES,
AddonModWorkshopData, } from '@addons/mod/workshop/constants';
AddonModWorkshopGetWorkshopAccessInformationWSResponse, import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
} from '../workshop'; import { CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
import { AddonModWorkshopHelper } from '../workshop-helper';
import { AddonModWorkshopSync } from '../workshop-sync';
/**
* Handler to prefetch workshops.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModWorkshop'; name = ADDON_MOD_WORKSHOP_PREFETCH_NAME;
modName = 'workshop'; modName = ADDON_MOD_WORKSHOP_PREFETCH_MODNAME;
component = AddonModWorkshopProvider.COMPONENT; component = ADDON_MOD_WORKSHOP_PREFETCH_COMPONENT;
updatesNames = new RegExp('^configuration$|^.*files$|^completion|^gradeitems$|^outcomes$|^submissions$|^assessments$' + updatesNames = ADDON_MOD_WORKSHOP_PREFETCH_UPDATE_NAMES;
'|^assessmentgrades$|^usersubmissions$|^userassessments$|^userassessmentgrades$|^userassessmentgrades$');
/**
* @inheritdoc
*/
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSFile[]> {
const info = await this.getWorkshopInfoHelper(module, courseId, { omitFail: true });
return info.files;
}
/**
* Helper function to get all workshop info just once.
*
* @param module Module to get the files.
* @param courseId Course ID the module belongs to.
* @param options Other options.
* @returns Promise resolved with the info fetched.
*/
protected async getWorkshopInfoHelper(
module: CoreCourseAnyModuleData,
courseId: number,
options: AddonModWorkshopGetInfoOptions = {},
): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
let groups: CoreGroup[] = [];
let files: CoreWSFile[] = [];
let workshop: AddonModWorkshopData;
let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
const modOptions = {
cmId: module.id,
...options, // Include all options.
};
const site = await CoreSites.getSite(options.siteId);
options.siteId = options.siteId ?? site.getId();
const userId = site.getUserId();
try {
workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
} catch (error) {
if (options.omitFail) {
// Any error, return the info we have.
return {
groups: [],
files: [],
};
}
throw error;
}
try {
files = this.getIntroFilesFromInstance(module, workshop);
files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
access = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions);
if (access.canviewallsubmissions) {
const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId);
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{ id: 0, name: '' }];
}
groups = groupInfo.groups;
}
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
// Get submission phase info.
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
const promises: Promise<void>[] = [];
if (canSubmit) {
promises.push(AddonModWorkshopHelper.getUserSubmission(workshop.id, {
userId,
cmId: module.id,
}).then((submission) => {
if (submission) {
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
}
return;
}));
}
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
promises.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions).then(async (submissions) => {
await Promise.all(submissions.map(async (submission) => {
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop.id, submission.id, {
cmId: module.id,
});
assessments.forEach((assessment) => {
files = files.concat(assessment.feedbackattachmentfiles)
.concat(assessment.feedbackcontentfiles);
});
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
await Promise.all(assessments.map((assessment) =>
AddonModWorkshopHelper.getReviewerAssessmentById(workshop.id, assessment.id)));
}
}));
return;
}));
}
// Get assessment files.
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
promises.push(AddonModWorkshopHelper.getReviewerAssessments(workshop.id, modOptions).then((assessments) => {
assessments.forEach((assessment) => {
files = files.concat(<CoreWSExternalFile[]>assessment.feedbackattachmentfiles)
.concat(assessment.feedbackcontentfiles);
});
return;
}));
}
await Promise.all(promises);
return {
workshop,
groups,
files: files.filter((file) => file !== undefined),
};
} catch (error) {
if (options.omitFail) {
// Any error, return the info we have.
return {
workshop,
groups,
files: files.filter((file) => file !== undefined),
};
}
throw error;
}
}
/**
* @inheritdoc
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModWorkshop.invalidateContent(moduleId, courseId);
}
/**
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @returns Whether the module can be downloaded. The promise should never be rejected.
*/
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, {
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
});
const accessData = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, { cmId: module.id });
// Check if workshop is setup by phase.
return accessData.canswitchphase || workshop.phase > AddonModWorkshopPhase.PHASE_SETUP;
}
/**
* @inheritdoc
*/
prefetch(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
return this.prefetchPackage(module, courseId, (siteId) => this.prefetchWorkshop(module, courseId, siteId));
}
/**
* Retrieves all the grades reports for all the groups and then returns only unique grades.
*
* @param workshopId Workshop ID.
* @param groups Array of groups in the activity.
* @param cmId Module ID.
* @param siteId Site ID. If not defined, current site.
* @returns All unique entries.
*/
protected async getAllGradesReport(
workshopId: number,
groups: CoreGroup[],
cmId: number,
siteId: string,
): Promise<AddonModWorkshopGradesData[]> {
const promises: Promise<AddonModWorkshopGradesData[]>[] = [];
groups.forEach((group) => {
promises.push(AddonModWorkshop.fetchAllGradeReports(workshopId, { groupId: group.id, cmId, siteId }));
});
const grades = await Promise.all(promises);
const uniqueGrades: Record<number, AddonModWorkshopGradesData> = {};
grades.forEach((groupGrades) => {
groupGrades.forEach((grade) => {
if (grade.submissionid) {
uniqueGrades[grade.submissionid] = grade;
}
});
});
return CoreUtils.objectToArray(uniqueGrades);
}
/**
* Prefetch a workshop.
*
* @param module The module object returned by WS.
* @param courseId Course ID the module belongs to.
* @param siteId Site ID.
* @returns Promise resolved when done.
*/
protected async prefetchWorkshop(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise<void> {
const userIds: number[] = [];
const commonOptions = {
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
siteId,
};
const modOptions = {
cmId: module.id,
...commonOptions, // Include all common options.
};
const site = await CoreSites.getSite(siteId);
const currentUserId = site.getUserId();
// Prefetch the workshop data.
const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
if (!info.workshop) {
// It would throw an exception so it would not happen.
return;
}
const workshop = info.workshop;
const promises: Promise<unknown>[] = [];
const assessmentIds: number[] = [];
promises.push(CoreFilepool.addFilesToQueue(siteId, info.files, this.component, module.id));
promises.push(AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions).then(async (access) => {
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
// Get submission phase info.
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
const promises2: Promise<unknown>[] = [];
if (canSubmit) {
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
// Add userId to the profiles to prefetch.
userIds.push(currentUserId);
}
let reportPromise: Promise<unknown> = Promise.resolve();
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
// eslint-disable-next-line promise/no-nesting
reportPromise = this.getAllGradesReport(workshop.id, info.groups, module.id, siteId).then((grades) => {
grades.forEach((grade) => {
userIds.push(grade.userid);
grade.submissiongradeoverby && userIds.push(grade.submissiongradeoverby);
grade.reviewedby && grade.reviewedby.forEach((assessment) => {
userIds.push(assessment.userid);
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
});
grade.reviewerof && grade.reviewerof.forEach((assessment) => {
userIds.push(assessment.userid);
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
});
});
return;
});
}
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
// Wait the report promise to finish to override assessments array if needed.
reportPromise = reportPromise.finally(async () => {
const revAssessments = await AddonModWorkshopHelper.getReviewerAssessments(workshop.id, {
userId: currentUserId,
cmId: module.id,
siteId,
});
let files: CoreWSExternalFile[] = []; // Files in each submission.
revAssessments.forEach((assessment) => {
if (assessment.submission?.authorid == currentUserId) {
promises.push(AddonModWorkshop.getAssessment(
workshop.id,
assessment.id,
modOptions,
));
}
userIds.push(assessment.reviewerid);
userIds.push(assessment.gradinggradeoverby);
assessmentIds[assessment.id] = assessment.id;
files = files.concat(assessment.submission?.attachmentfiles || [])
.concat(assessment.submission?.contentfiles || []);
});
await CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id);
});
}
reportPromise = reportPromise.finally(() => {
if (assessmentIds.length > 0) {
return Promise.all(assessmentIds.map((assessmentId) =>
AddonModWorkshop.getAssessmentForm(workshop.id, assessmentId, modOptions)));
}
});
promises2.push(reportPromise);
if (workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
promises2.push(AddonModWorkshop.getGrades(workshop.id, modOptions));
if (access.canviewpublishedsubmissions) {
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
}
}
await Promise.all(promises2);
return;
}));
// Add Basic Info to manage links.
promises.push(CoreCourse.getModuleBasicInfoByInstance(workshop.id, 'workshop', { siteId }));
promises.push(CoreCourse.getModuleBasicGradeInfo(module.id, siteId));
// Get course data, needed to determine upload max size if it's configured to be course limit.
promises.push(CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', courseId, siteId)));
await Promise.all(promises);
// Prefetch user profiles.
await CoreUser.prefetchProfiles(userIds, courseId, siteId);
}
/**
* @inheritdoc
*/
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModDataSyncResult> {
return AddonModWorkshopSync.syncWorkshop(module.instance, siteId);
}
} }
export const AddonModWorkshopPrefetchHandler = makeSingleton(AddonModWorkshopPrefetchHandlerService);
/** /**
* Options to pass to getWorkshopInfoHelper. * Get prefetch handler instance.
*
* @returns Prefetch handler.
*/ */
export type AddonModWorkshopGetInfoOptions = CoreSitesCommonWSOptions & { export function getPrefetchHandlerInstance(): CoreCourseModulePrefetchHandler {
omitFail?: boolean; // True to always return even if fails. const lazyHandler = asyncInstance(async () => {
}; const { AddonModWorkshopPrefetchHandler } = await import('./prefetch-lazy');
return AddonModWorkshopPrefetchHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopPrefetchHandlerService());
return lazyHandler;
}

View File

@ -0,0 +1,44 @@
// (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 { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { AddonModWorkshopSync } from '../workshop-sync';
import { AddonModWorkshopSyncCronHandlerService } from './sync-cron';
/**
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopSyncCronHandlerLazyService
extends AddonModWorkshopSyncCronHandlerService
implements CoreCronHandler {
/**
* @inheritdoc
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return AddonModWorkshopSync.syncAllWorkshops(siteId, force);
}
/**
* @inheritdoc
*/
getInterval(): number {
return AddonModWorkshopSync.syncInterval;
}
}
export const AddonModWorkshopSyncCronHandler = makeSingleton(AddonModWorkshopSyncCronHandlerLazyService);

View File

@ -12,32 +12,29 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { asyncInstance } from '@/core/utils/async-instance';
import { ADDON_MOD_WORKSHOP_SYNC_CRON_NAME } from '@addons/mod/workshop/constants';
import { CoreCronHandler } from '@services/cron'; import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { AddonModWorkshopSync } from '../workshop-sync';
/** export class AddonModWorkshopSyncCronHandlerService {
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonModWorkshopSyncCronHandlerService implements CoreCronHandler {
name = 'AddonModWorkshopSyncCronHandler'; name = ADDON_MOD_WORKSHOP_SYNC_CRON_NAME;
/**
* @inheritdoc
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return AddonModWorkshopSync.syncAllWorkshops(siteId, force);
}
/**
* @inheritdoc
*/
getInterval(): number {
return AddonModWorkshopSync.syncInterval;
}
} }
export const AddonModWorkshopSyncCronHandler = makeSingleton(AddonModWorkshopSyncCronHandlerService);
/**
* Get cron handler instance.
*
* @returns Cron handler.
*/
export function getCronHandlerInstance(): CoreCronHandler {
const lazyHandler = asyncInstance(async () => {
const { AddonModWorkshopSyncCronHandler } = await import('./sync-cron-lazy');
return AddonModWorkshopSyncCronHandler.instance;
});
lazyHandler.setEagerInstance(new AddonModWorkshopSyncCronHandlerService());
return lazyHandler;
}

View File

@ -29,7 +29,6 @@ import {
AddonModWorkshopExampleMode, AddonModWorkshopExampleMode,
AddonModWorkshopPhase, AddonModWorkshopPhase,
AddonModWorkshopUserOptions, AddonModWorkshopUserOptions,
AddonModWorkshopProvider,
AddonModWorkshopData, AddonModWorkshopData,
AddonModWorkshop, AddonModWorkshop,
AddonModWorkshopSubmissionData, AddonModWorkshopSubmissionData,
@ -42,6 +41,7 @@ import {
AddonModWorkshopGetAssessmentFormFieldsParsedData, AddonModWorkshopGetAssessmentFormFieldsParsedData,
} from './workshop'; } from './workshop';
import { AddonModWorkshopOffline, AddonModWorkshopOfflineSubmission } from './workshop-offline'; import { AddonModWorkshopOffline, AddonModWorkshopOfflineSubmission } from './workshop-offline';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Helper to gather some common functions for workshop. * Helper to gather some common functions for workshop.
@ -293,7 +293,7 @@ export class AddonModWorkshopHelperProvider {
return this.storeSubmissionFiles(workshopId, files, siteId); return this.storeSubmissionFiles(workshopId, files, siteId);
} }
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId); return CoreFileUploader.uploadOrReuploadFiles(files, ADDON_MOD_WORKSHOP_COMPONENT, workshopId, siteId);
} }
/** /**
@ -403,7 +403,7 @@ export class AddonModWorkshopHelperProvider {
return this.storeAssessmentFiles(workshopId, assessmentId, files, siteId); return this.storeAssessmentFiles(workshopId, assessmentId, files, siteId);
} }
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId); return CoreFileUploader.uploadOrReuploadFiles(files, ADDON_MOD_WORKSHOP_COMPONENT, workshopId, siteId);
} }
/** /**

View File

@ -28,7 +28,6 @@ import { CoreEvents } from '@singletons/events';
import { AddonModWorkshop, import { AddonModWorkshop,
AddonModWorkshopAction, AddonModWorkshopAction,
AddonModWorkshopData, AddonModWorkshopData,
AddonModWorkshopProvider,
AddonModWorkshopSubmissionType, AddonModWorkshopSubmissionType,
} from './workshop'; } from './workshop';
import { AddonModWorkshopHelper } from './workshop-helper'; import { AddonModWorkshopHelper } from './workshop-helper';
@ -38,6 +37,7 @@ import { AddonModWorkshopOffline,
AddonModWorkshopOfflineEvaluateSubmission, AddonModWorkshopOfflineEvaluateSubmission,
AddonModWorkshopOfflineSubmission, AddonModWorkshopOfflineSubmission,
} from './workshop-offline'; } from './workshop-offline';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
/** /**
* Service to sync workshops. * Service to sync workshops.
@ -136,7 +136,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider<AddonModW
} }
// Verify that workshop isn't blocked. // Verify that workshop isn't blocked.
if (CoreSync.isBlocked(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)) { if (CoreSync.isBlocked(ADDON_MOD_WORKSHOP_COMPONENT, workshopId, siteId)) {
this.logger.debug(`Cannot sync workshop '${workshopId}' because it is blocked.`); this.logger.debug(`Cannot sync workshop '${workshopId}' because it is blocked.`);
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
@ -163,7 +163,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider<AddonModW
}; };
// Sync offline logs. // Sync offline logs.
await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)); await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(ADDON_MOD_WORKSHOP_COMPONENT, workshopId, siteId));
// Get offline submissions to be sent. // Get offline submissions to be sent.
const syncs = await Promise.all([ const syncs = await Promise.all([

View File

@ -27,6 +27,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreFormFields } from '@singletons/form'; import { CoreFormFields } from '@singletons/form';
import { AddonModWorkshopOffline } from './workshop-offline'; import { AddonModWorkshopOffline } from './workshop-offline';
import { AddonModWorkshopAutoSyncData, AddonModWorkshopSyncProvider } from './workshop-sync'; import { AddonModWorkshopAutoSyncData, AddonModWorkshopSyncProvider } from './workshop-sync';
import { ADDON_MOD_WORKSHOP_COMPONENT } from '@addons/mod/workshop/constants';
const ROOT_CACHE_KEY = 'mmaModWorkshop:'; const ROOT_CACHE_KEY = 'mmaModWorkshop:';
@ -88,7 +89,6 @@ declare module '@singletons/events' {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AddonModWorkshopProvider { export class AddonModWorkshopProvider {
static readonly COMPONENT = 'mmaModWorkshop';
static readonly PER_PAGE = 10; static readonly PER_PAGE = 10;
static readonly SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed'; static readonly SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed';
@ -248,7 +248,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getWorkshopDataCacheKey(courseId), cacheKey: this.getWorkshopDataCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY, updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
const response = await site.read<AddonModWorkshopGetWorkshopsByCoursesWSResponse>( const response = await site.read<AddonModWorkshopGetWorkshopsByCoursesWSResponse>(
@ -345,7 +345,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId), cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -390,7 +390,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserPlanDataCacheKey(workshopId), cacheKey: this.getUserPlanDataCacheKey(workshopId),
updateFrequency: CoreSite.FREQUENCY_OFTEN, updateFrequency: CoreSite.FREQUENCY_OFTEN,
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -437,7 +437,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getSubmissionsDataCacheKey(workshopId, userId, groupId), cacheKey: this.getSubmissionsDataCacheKey(workshopId, userId, groupId),
updateFrequency: CoreSite.FREQUENCY_OFTEN, updateFrequency: CoreSite.FREQUENCY_OFTEN,
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -483,7 +483,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId), cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -523,7 +523,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getGradesDataCacheKey(workshopId), cacheKey: this.getGradesDataCacheKey(workshopId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -567,7 +567,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getGradesReportDataCacheKey(workshopId, options.groupId), cacheKey: this.getGradesReportDataCacheKey(workshopId, options.groupId),
updateFrequency: CoreSite.FREQUENCY_OFTEN, updateFrequency: CoreSite.FREQUENCY_OFTEN,
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -663,7 +663,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId), cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -967,7 +967,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, options.userId), cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, options.userId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -1017,7 +1017,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId), cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId),
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -1065,7 +1065,7 @@ export class AddonModWorkshopProvider {
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode), cacheKey: this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode),
updateFrequency: CoreSite.FREQUENCY_RARELY, updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModWorkshopProvider.COMPONENT, component: ADDON_MOD_WORKSHOP_COMPONENT,
componentId: options.cmId, componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
}; };
@ -1454,7 +1454,7 @@ export class AddonModWorkshopProvider {
await CoreCourseLogHelper.log( await CoreCourseLogHelper.log(
'mod_workshop_view_workshop', 'mod_workshop_view_workshop',
params, params,
AddonModWorkshopProvider.COMPONENT, ADDON_MOD_WORKSHOP_COMPONENT,
id, id,
siteId, siteId,
); );
@ -1476,7 +1476,7 @@ export class AddonModWorkshopProvider {
await CoreCourseLogHelper.log( await CoreCourseLogHelper.log(
'mod_workshop_view_submission', 'mod_workshop_view_submission',
params, params,
AddonModWorkshopProvider.COMPONENT, ADDON_MOD_WORKSHOP_COMPONENT,
workshopId, workshopId,
siteId, siteId,
); );

View File

@ -27,13 +27,14 @@ import { AddonWorkshopAssessmentStrategyDelegateService } from './services/asses
import { ADDON_MOD_WORKSHOP_OFFLINE_SITE_SCHEMA } from './services/database/workshop'; import { ADDON_MOD_WORKSHOP_OFFLINE_SITE_SCHEMA } from './services/database/workshop';
import { AddonModWorkshopIndexLinkHandler } from './services/handlers/index-link'; import { AddonModWorkshopIndexLinkHandler } from './services/handlers/index-link';
import { AddonModWorkshopListLinkHandler } from './services/handlers/list-link'; import { AddonModWorkshopListLinkHandler } from './services/handlers/list-link';
import { AddonModWorkshopModuleHandler, AddonModWorkshopModuleHandlerService } from './services/handlers/module'; import { AddonModWorkshopModuleHandler } from './services/handlers/module';
import { AddonModWorkshopPrefetchHandler } from './services/handlers/prefetch';
import { AddonModWorkshopSyncCronHandler } from './services/handlers/sync-cron';
import { AddonModWorkshopProvider } from './services/workshop'; import { AddonModWorkshopProvider } from './services/workshop';
import { AddonModWorkshopHelperProvider } from './services/workshop-helper'; import { AddonModWorkshopHelperProvider } from './services/workshop-helper';
import { AddonModWorkshopOfflineProvider } from './services/workshop-offline'; import { AddonModWorkshopOfflineProvider } from './services/workshop-offline';
import { AddonModWorkshopSyncProvider } from './services/workshop-sync'; import { AddonModWorkshopSyncProvider } from './services/workshop-sync';
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';
// List of providers (without handlers). // List of providers (without handlers).
export const ADDON_MOD_WORKSHOP_SERVICES: Type<unknown>[] = [ export const ADDON_MOD_WORKSHOP_SERVICES: Type<unknown>[] = [
@ -46,7 +47,7 @@ export const ADDON_MOD_WORKSHOP_SERVICES: Type<unknown>[] = [
const routes: Routes = [ const routes: Routes = [
{ {
path: AddonModWorkshopModuleHandlerService.PAGE_NAME, path: ADDON_MOD_WORKSHOP_PAGE_NAME,
loadChildren: () => import('./workshop-lazy.module').then(m => m.AddonModWorkshopLazyModule), loadChildren: () => import('./workshop-lazy.module').then(m => m.AddonModWorkshopLazyModule),
}, },
]; ];
@ -68,12 +69,12 @@ const routes: Routes = [
multi: true, multi: true,
useValue: () => { useValue: () => {
CoreCourseModuleDelegate.registerHandler(AddonModWorkshopModuleHandler.instance); CoreCourseModuleDelegate.registerHandler(AddonModWorkshopModuleHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModWorkshopPrefetchHandler.instance); CoreCourseModulePrefetchDelegate.registerHandler(getPrefetchHandlerInstance());
CoreCronDelegate.register(AddonModWorkshopSyncCronHandler.instance); CoreCronDelegate.register(getCronHandlerInstance());
CoreContentLinksDelegate.registerHandler(AddonModWorkshopIndexLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModWorkshopIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModWorkshopListLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModWorkshopListLinkHandler.instance);
CoreCourseHelper.registerModuleReminderClick(AddonModWorkshopProvider.COMPONENT); CoreCourseHelper.registerModuleReminderClick(ADDON_MOD_WORKSHOP_COMPONENT);
}, },
}, },
], ],

View File

@ -23,7 +23,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CoreNativeService { export class CoreNativeService {
private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance<unknown>>> = {}; private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {};
/** /**
* Get a native plugin instance. * Get a native plugin instance.

View File

@ -212,14 +212,14 @@ export class CoreCronDelegateService {
* @param name Handler's name. * @param name Handler's name.
* @returns Handler's interval. * @returns Handler's interval.
*/ */
protected getHandlerInterval(name: string): number { protected async getHandlerInterval(name: string): Promise<number> {
if (this.handlers[name] === undefined) { if (this.handlers[name] === undefined) {
// Invalid, return default. // Invalid, return default.
return CoreCronDelegateService.DEFAULT_INTERVAL; return CoreCronDelegateService.DEFAULT_INTERVAL;
} }
// Don't allow intervals lower than the minimum. // Don't allow intervals lower than the minimum.
const handlerInterval = this.handlers[name].getInterval?.(); const handlerInterval = await this.handlers[name].getInterval?.();
if (!handlerInterval) { if (!handlerInterval) {
return CoreCronDelegateService.DEFAULT_INTERVAL; return CoreCronDelegateService.DEFAULT_INTERVAL;
@ -365,8 +365,7 @@ export class CoreCronDelegateService {
if (!timeToNextExecution) { if (!timeToNextExecution) {
// Get last execution time to check when do we need to execute it. // Get last execution time to check when do we need to execute it.
const lastExecution = await this.getHandlerLastExecutionTime(name); const lastExecution = await this.getHandlerLastExecutionTime(name);
const interval = await this.getHandlerInterval(name);
const interval = this.getHandlerInterval(name);
timeToNextExecution = lastExecution + interval - Date.now(); timeToNextExecution = lastExecution + interval - Date.now();
} }
@ -486,7 +485,7 @@ export interface CoreCronHandler {
* *
* @returns Interval time (in milliseconds). * @returns Interval time (in milliseconds).
*/ */
getInterval?(): number; getInterval?(): number | Promise<number>;
/** /**
* Check whether the process uses network or not. True if not defined. * Check whether the process uses network or not. True if not defined.

View File

@ -14,19 +14,28 @@
import { CorePromisedValue } from '@classes/promised-value'; 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. * Create a wrapper to hold an asynchronous instance.
* *
* @param lazyConstructor Constructor to use the first time the instance is needed. * @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance wrapper. * @returns Asynchronous instance wrapper.
*/ */
function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstanceWrapper<T> { function createAsyncInstanceWrapper<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance>(
let promisedInstance: CorePromisedValue<T> | null = null; lazyConstructor?: () => TInstance | Promise<TInstance>,
): AsyncInstanceWrapper<TEagerInstance, TInstance> {
let promisedInstance: CorePromisedValue<TInstance> | null = null;
let eagerInstance: TEagerInstance;
return { return {
get instance() { get instance() {
return promisedInstance?.value ?? undefined; return promisedInstance?.value ?? undefined;
}, },
get eagerInstance() {
return eagerInstance;
},
async getInstance() { async getInstance() {
if (!promisedInstance) { if (!promisedInstance) {
promisedInstance = new CorePromisedValue(); promisedInstance = new CorePromisedValue();
@ -54,6 +63,9 @@ function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>):
promisedInstance.resolve(instance); promisedInstance.resolve(instance);
}, },
setEagerInstance(instance) {
eagerInstance = instance;
},
setLazyConstructor(constructor) { setLazyConstructor(constructor) {
if (!promisedInstance) { if (!promisedInstance) {
lazyConstructor = constructor; lazyConstructor = constructor;
@ -81,12 +93,14 @@ function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>):
/** /**
* Asynchronous instance wrapper. * Asynchronous instance wrapper.
*/ */
export interface AsyncInstanceWrapper<T> { export interface AsyncInstanceWrapper<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance> {
instance?: T; instance?: TInstance;
getInstance(): Promise<T>; eagerInstance?: TEagerInstance;
getProperty<P extends keyof T>(property: P): Promise<T[P]>; getInstance(): Promise<TInstance>;
setInstance(instance: T): void; getProperty<P extends keyof TInstance>(property: P): Promise<TInstance[P]>;
setLazyConstructor(lazyConstructor: () => T | Promise<T>): void; setInstance(instance: TInstance): void;
setEagerInstance(eagerInstance: TEagerInstance): void;
setLazyConstructor(lazyConstructor: () => TInstance | Promise<TInstance>): void;
resetInstance(): void; resetInstance(): void;
} }
@ -107,9 +121,10 @@ export type AsyncMethod<T> =
* All methods are converted to their asynchronous version, and properties are available asynchronously using * All methods are converted to their asynchronous version, and properties are available asynchronously using
* the getProperty method. * the getProperty method.
*/ */
export type AsyncInstance<T> = AsyncInstanceWrapper<T> & { export type AsyncInstance<TEagerInstance extends AsyncObject = AsyncObject, TInstance extends TEagerInstance = TEagerInstance> =
[k in keyof T]: AsyncMethod<T[k]>; AsyncInstanceWrapper<TEagerInstance, TInstance> & TEagerInstance & {
}; [k in keyof TInstance]: AsyncMethod<TInstance[k]>;
};
/** /**
* Create an asynchronous instance proxy, where all methods will be callable directly but will become asynchronous. If the * Create an asynchronous instance proxy, where all methods will be callable directly but will become asynchronous. If the
@ -118,8 +133,10 @@ export type AsyncInstance<T> = AsyncInstanceWrapper<T> & {
* @param lazyConstructor Constructor to use the first time the instance is needed. * @param lazyConstructor Constructor to use the first time the instance is needed.
* @returns Asynchronous instance. * @returns Asynchronous instance.
*/ */
export function asyncInstance<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstance<T> { export function asyncInstance<TEagerInstance extends AsyncObject, TInstance extends TEagerInstance = TEagerInstance>(
const wrapper = createAsyncInstanceWrapper<T>(lazyConstructor); lazyConstructor?: () => TInstance | Promise<TInstance>,
): AsyncInstance<TEagerInstance, TInstance> {
const wrapper = createAsyncInstanceWrapper<TEagerInstance, TInstance>(lazyConstructor);
return new Proxy(wrapper, { return new Proxy(wrapper, {
get: (target, property, receiver) => { get: (target, property, receiver) => {
@ -127,11 +144,19 @@ export function asyncInstance<T>(lazyConstructor?: () => T | Promise<T>): AsyncI
return Reflect.get(target, property, receiver); return Reflect.get(target, property, receiver);
} }
if (wrapper.instance && property in wrapper.instance) {
return Reflect.get(wrapper.instance, property, receiver);
}
if (wrapper.eagerInstance && property in wrapper.eagerInstance) {
return Reflect.get(wrapper.eagerInstance, property, receiver);
}
return async (...args: unknown[]) => { return async (...args: unknown[]) => {
const instance = await wrapper.getInstance(); const instance = await wrapper.getInstance();
return instance[property](...args); return instance[property](...args);
}; };
}, },
}) as AsyncInstance<T>; }) as AsyncInstance<TEagerInstance, TInstance>;
} }