-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
delegate.ts
423 lines (360 loc) · 13.3 KB
/
delegate.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// (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';
import { CoreEvents } from '@singletons/events';
import { CoreSite } from '@classes/sites/site';
import { CoreLogger } from '@singletons/logger';
/**
* Superclass to help creating delegates
*/
export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
/**
* Logger instance.
*/
protected logger: CoreLogger;
/**
* List of registered handlers.
*/
protected handlers: { [s: string]: HandlerType } = {};
/**
* List of registered handlers enabled for the current site.
*/
protected enabledHandlers: { [s: string]: HandlerType } = {};
/**
* Default handler
*/
protected defaultHandler?: HandlerType;
/**
* Time when last updateHandler functions started.
*/
protected lastUpdateHandlersStart = 0;
/**
* Feature prefix to check is feature is enabled or disabled in site.
* This check is only made if not false. Override on the subclass or override isFeatureDisabled function.
*/
protected featurePrefix?: string;
/**
* Name of the property to be used to index the handlers. By default, the handler's name will be used.
* If your delegate uses a Moodle component name to identify the handlers, please override this property.
* E.g. CoreCourseModuleDelegate uses 'modName' to index the handlers.
*/
protected handlerNameProperty = 'name';
/**
* Set of promises to update a handler, to prevent doing the same operation twice.
*/
protected updatePromises: {[siteId: string]: {[name: string]: Promise<void>}} = {};
/**
* Whether handlers have been initialized.
*/
protected handlersInitialized = false;
/**
* Promise to wait for handlers to be initialized.
*
* @returns Promise resolved when handlers are enabled.
*/
protected handlersInitPromise: Promise<boolean>;
/**
* Function to resolve the handlers init promise.
*/
protected handlersInitResolve!: (enabled: boolean) => void;
/**
* Constructor of the Delegate.
*
* @param delegateName Delegate name used for logging purposes.
*/
constructor(delegateName: string) {
this.logger = CoreLogger.getInstance(delegateName);
this.handlersInitPromise = new Promise((resolve): void => {
this.handlersInitResolve = resolve;
});
// Update handlers on this cases.
CoreEvents.on(CoreEvents.LOGIN, () => this.updateHandlers());
CoreEvents.on(CoreEvents.SITE_UPDATED, () => this.updateHandlers());
CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => this.updateHandlers());
CoreEvents.on(CoreEvents.SITE_POLICY_AGREED, (data) => {
if (data.siteId === CoreSites.getCurrentSiteId()) {
this.updateHandlers();
}
});
CoreEvents.on(CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED, (data) => {
if (data.siteId === CoreSites.getCurrentSiteId()) {
this.updateHandlers();
}
});
}
/**
* Check if the delegate is enabled so handlers are not updated if not..
*
* @returns Whether the delegate is enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Execute a certain function in a enabled handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @returns Function returned value or default value.
*/
protected executeFunctionOnEnabled<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
return this.execute<T>(this.enabledHandlers[handlerName], fnName, params);
}
/**
* Execute a certain function in a handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @returns Function returned value or default value.
*/
protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
return this.execute<T>(this.handlers[handlerName], fnName, params);
}
/**
* Execute a certain function in a handler.
* If the handler isn't found or function isn't defined, call the same function in the default handler.
*
* @param handler The handler.
* @param fnName Name of the function to execute.
* @param params Parameters to pass to the function.
* @returns Function returned value or default value.
*/
private execute<T = unknown>(handler: HandlerType, fnName: string, params?: unknown[]): T | undefined {
if (handler && handler[fnName]) {
return handler[fnName].apply(handler, params);
} else if (this.defaultHandler && this.defaultHandler[fnName]) {
return this.defaultHandler[fnName].apply(this.defaultHandler, params);
}
}
/**
* Get a handler.
*
* @param handlerName The handler name.
* @param enabled Only enabled, or any.
* @returns Handler.
*/
protected getHandler(handlerName: string, enabled: boolean = false): HandlerType {
return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
}
/**
* Gets the handler full name for a given name. This is useful when the handlerNameProperty is different than "name".
* E.g. blocks are indexed by blockName. If you call this function passing the blockName it will return the name.
*
* @param name Name used to indentify the handler.
* @returns Full name of corresponding handler.
*/
getHandlerName(name: string): string {
const handler = this.getHandler(name, true);
if (!handler) {
return '';
}
return handler.name;
}
/**
* Check if function exists on a handler.
*
* @param handlerName The handler name.
* @param fnName Name of the function to execute.
* @param onlyEnabled If check only enabled handlers or all.
* @returns Function returned value or default value.
*/
protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): boolean {
const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
return handler && typeof handler[fnName] == 'function';
}
/**
* Check if a handler name has a registered handler (not necessarily enabled).
*
* @param name The handler name.
* @param enabled Only enabled, or any.
* @returns If the handler is registered or not.
*/
hasHandler(name: string, enabled: boolean = false): boolean {
return enabled ? this.enabledHandlers[name] !== undefined : this.handlers[name] !== undefined;
}
/**
* Check if the delegate has at least 1 registered handler (not necessarily enabled).
*
* @returns If there is at least 1 handler.
*/
hasHandlers(): boolean {
return Object.keys(this.handlers).length > 0;
}
/**
* Check if a time belongs to the last update handlers call.
* This is to handle the cases where updateHandlers don't finish in the same order as they're called.
*
* @param time Time to check.
* @returns Whether it's the last call.
*/
isLastUpdateCall(time: number): boolean {
if (!this.lastUpdateHandlersStart) {
return true;
}
return time == this.lastUpdateHandlersStart;
}
/**
* Register a handler.
*
* @param handler The handler delegate object to register.
* @returns True when registered, false if already registered.
*/
registerHandler(handler: HandlerType): boolean {
const key = handler[this.handlerNameProperty] || handler.name;
if (this.handlers[key] !== undefined) {
this.logger.log(`Handler '${key}' already registered`);
return false;
}
this.logger.log(`Registered handler '${key}'`);
this.handlers[key] = handler;
return true;
}
/**
* Update the handler for the current site.
*
* @param handler The handler to check.
* @returns Resolved when done.
*/
protected updateHandler(handler: HandlerType): Promise<void> {
const siteId = CoreSites.getCurrentSiteId();
const currentSite = CoreSites.getCurrentSite();
let promise: Promise<boolean>;
if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name] !== undefined) {
// There's already an update ongoing for this handler, return the promise.
return this.updatePromises[siteId][handler.name];
} else if (!this.updatePromises[siteId]) {
this.updatePromises[siteId] = {};
}
if (!currentSite || this.isFeatureDisabled(handler, currentSite)) {
promise = Promise.resolve(false);
} else {
promise = Promise.resolve(handler.isEnabled()).catch(() => false);
}
// Checks if the handler is enabled.
this.updatePromises[siteId][handler.name] = promise.then((enabled) => {
// Check that site hasn't changed since the check started.
if (CoreSites.getCurrentSiteId() !== siteId) {
return;
}
const key = handler[this.handlerNameProperty] || handler.name;
if (enabled) {
this.enabledHandlers[key] = handler;
} else {
delete this.enabledHandlers[key];
}
return;
}).finally(() => {
// Update finished, delete the promise.
delete this.updatePromises[siteId][handler.name];
});
return this.updatePromises[siteId][handler.name];
}
/**
* Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name.
*
* @param handler Handler to check.
* @param site Site to check.
* @returns Whether is enabled or disabled in site.
*/
protected isFeatureDisabled(handler: HandlerType, site: CoreSite): boolean {
return this.featurePrefix !== undefined && site.isFeatureDisabled(this.featurePrefix + handler.name);
}
/**
* Update the handlers for the current site.
*
* @returns Resolved when done.
*/
async updateHandlers(): Promise<void> {
const enabled = await this.isEnabled();
if (!enabled) {
this.logger.debug('Delegate not enabled.');
this.handlersInitResolve(false);
return;
}
const promises: Promise<void>[] = [];
const now = Date.now();
this.logger.debug('Updating handlers for current site.');
this.lastUpdateHandlersStart = now;
// Loop over all the handlers.
for (const name in this.handlers) {
promises.push(this.updateHandler(this.handlers[name]));
}
try {
await Promise.all(promises);
} catch {
// Never reject
}
// Verify that this call is the last one that was started.
if (this.isLastUpdateCall(now)) {
this.handlersInitialized = true;
this.handlersInitResolve(true);
this.updateData();
}
}
/**
* Update handlers Data.
* Override this function to update handlers data.
*/
updateData(): void {
// To be overridden.
}
}
/**
* Base interface for any delegate.
*/
export interface CoreDelegateHandler {
/**
* Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...).
* This name will be used to check if the feature is disabled.
*/
name: string;
/**
* Whether or not the handler is enabled on a site level.
*
* @returns Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean>;
}
/**
* Data returned by the delegate for each handler to be displayed.
*/
export interface CoreDelegateToDisplay {
/**
* Name of the handler.
*/
name?: string;
/**
* Priority of the handler.
*/
priority?: number;
}
/**
* Base interface for a core delegate needed to be displayed.
*/
export interface CoreDelegateDisplayHandler<HandlerData extends CoreDelegateToDisplay> extends CoreDelegateHandler {
/**
* The highest priority is displayed first.
*/
priority?: number;
/**
* Returns the data needed to render the handler.
*
* @returns Data.
*/
getDisplayData(): HandlerData;
}