Merge pull request #3181 from NoelDeMartin/MOBILE-3833

MOBILE-3833 core: Implement cancellable promise
main
Pau Ferrer Ocaña 2022-03-16 09:59:48 +01:00 committed by GitHub
commit 606f43b526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 208 additions and 127 deletions

View File

@ -0,0 +1,56 @@
// (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 { CorePromise } from '@classes/promise';
/**
* Promise whose execution can be cancelled.
*/
export class CoreCancellablePromise<T = unknown> extends CorePromise<T> {
/**
* Create a new resolved promise.
*
* @returns Resolved promise.
*/
static resolve(): CoreCancellablePromise<void>;
static resolve<T>(result: T): CoreCancellablePromise<T>;
static resolve<T>(result?: T): CoreCancellablePromise<T> {
return new this(resolve => result ? resolve(result) : (resolve as () => void)(), () => {
// Nothing to do here.
});
}
protected cancelPromise: () => void;
constructor(
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: Error) => void) => void,
cancelPromise: () => void,
) {
super(new Promise(executor));
this.cancelPromise = cancelPromise;
}
/**
* Cancel promise.
*
* After this method is called, the promise will remain unresolved forever. Make sure that after calling
* this method there aren't any references to this object, or it could cause memory leaks.
*/
cancel(): void {
this.cancelPromise();
}
}

View File

@ -0,0 +1,54 @@
// (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.
/**
* Base class to use for implementing custom Promises.
*/
export abstract class CorePromise<T = unknown> implements Promise<T> {
protected nativePromise: Promise<T>;
constructor(nativePromise: Promise<T>) {
this.nativePromise = nativePromise;
}
[Symbol.toStringTag]: string;
/**
* @inheritdoc
*/
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.nativePromise.then(onFulfilled, onRejected);
}
/**
* @inheritdoc
*/
catch<TResult = never>(
onRejected?: ((reason: Error) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.nativePromise.catch(onRejected);
}
/**
* @inheritdoc
*/
finally(onFinally?: (() => void) | null): Promise<T> {
return this.nativePromise.finally(onFinally);
}
}

View File

@ -12,10 +12,12 @@
// 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 { CorePromise } from '@classes/promise';
/** /**
* Promise wrapper to expose result synchronously. * Promise wrapper to expose result synchronously.
*/ */
export class CorePromisedValue<T = unknown> implements Promise<T> { export class CorePromisedValue<T = unknown> extends CorePromise<T> {
/** /**
* Wrap an existing promise. * Wrap an existing promise.
@ -33,20 +35,28 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
return promisedValue; return promisedValue;
} }
private _resolvedValue?: T; protected resolvedValue?: T;
private _rejectedReason?: Error; protected rejectedReason?: Error;
declare private promise: Promise<T>; protected resolvePromise!: (result: T) => void;
declare private _resolve: (result: T) => void; protected rejectPromise!: (error?: Error) => void;
declare private _reject: (error?: Error) => void;
constructor() { constructor() {
this.initPromise(); let resolvePromise!: (result: T) => void;
let rejectPromise!: (error?: Error) => void;
const nativePromise = new Promise<T>((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
super(nativePromise);
this.resolvePromise = resolvePromise;
this.rejectPromise = rejectPromise;
} }
[Symbol.toStringTag]: string;
get value(): T | null { get value(): T | null {
return this._resolvedValue ?? null; return this.resolvedValue ?? null;
} }
/** /**
@ -55,7 +65,7 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
* @return Whether the promise resolved successfuly. * @return Whether the promise resolved successfuly.
*/ */
isResolved(): this is { value: T } { isResolved(): this is { value: T } {
return '_resolvedValue' in this; return 'resolvedValue' in this;
} }
/** /**
@ -64,7 +74,7 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
* @return Whether the promise was rejected. * @return Whether the promise was rejected.
*/ */
isRejected(): boolean { isRejected(): boolean {
return '_rejectedReason' in this; return 'rejectedReason' in this;
} }
/** /**
@ -76,32 +86,6 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
return this.isResolved() || this.isRejected(); return this.isResolved() || this.isRejected();
} }
/**
* @inheritdoc
*/
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}
/**
* @inheritdoc
*/
catch<TResult = never>(
onRejected?: ((reason: Error) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}
/**
* @inheritdoc
*/
finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}
/** /**
* Resolve the promise. * Resolve the promise.
* *
@ -109,13 +93,13 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
*/ */
resolve(value: T): void { resolve(value: T): void {
if (this.isSettled()) { if (this.isSettled()) {
delete this._rejectedReason; delete this.rejectedReason;
this.initPromise(); this.resetNativePromise();
} }
this._resolvedValue = value; this.resolvedValue = value;
this._resolve(value); this.resolvePromise(value);
} }
/** /**
@ -125,32 +109,32 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
*/ */
reject(reason?: Error): void { reject(reason?: Error): void {
if (this.isSettled()) { if (this.isSettled()) {
delete this._resolvedValue; delete this.resolvedValue;
this.initPromise(); this.resetNativePromise();
} }
this._rejectedReason = reason; this.rejectedReason = reason;
this._reject(reason); this.rejectPromise(reason);
} }
/** /**
* Reset status and value. * Reset status and value.
*/ */
reset(): void { reset(): void {
delete this._resolvedValue; delete this.resolvedValue;
delete this._rejectedReason; delete this.rejectedReason;
this.initPromise(); this.resetNativePromise();
} }
/** /**
* Initialize the promise and the callbacks. * Reset native promise and callbacks.
*/ */
private initPromise(): void { protected resetNativePromise(): void {
this.promise = new Promise((resolve, reject) => { this.nativePromise = new Promise((resolve, reject) => {
this._resolve = resolve; this.resolvePromise = resolve;
this._reject = reject; this.rejectPromise = reject;
}); });
} }

View File

@ -19,9 +19,10 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreMath } from '@singletons/math'; import { CoreMath } from '@singletons/math';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreFormatTextDirective } from './format-text'; import { CoreFormatTextDirective } from './format-text';
import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
/** /**
* Directive to make an element fixed at the bottom collapsible when scrolling. * Directive to make an element fixed at the bottom collapsible when scrolling.
@ -48,7 +49,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
protected contentScrollListener?: EventListener; protected contentScrollListener?: EventListener;
protected endContentScrollListener?: EventListener; protected endContentScrollListener?: EventListener;
protected resizeListener?: CoreEventObserver; protected resizeListener?: CoreEventObserver;
protected domListener?: CoreSingleTimeEventObserver; protected domPromise?: CoreCancellablePromise<void>;
constructor(el: ElementRef, protected ionContent: IonContent) { constructor(el: ElementRef, protected ionContent: IonContent) {
this.element = el.nativeElement; this.element = el.nativeElement;
@ -61,10 +62,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// Only if not present or explicitly falsy it will be false. // Only if not present or explicitly falsy it will be false.
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom); this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
this.domListener = CoreDomUtils.waitToBeInDOM(this.element); await this.domPromise;
await this.domListener.promise;
await this.waitLoadingsDone(); await this.waitLoadingsDone();
await this.waitFormatTextsRendered(this.element); await this.waitFormatTextsRendered(this.element);
@ -234,7 +234,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
} }
this.resizeListener?.off(); this.resizeListener?.off();
this.domListener?.off(); this.domPromise?.cancel();
} }
} }

View File

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreFormatTextDirective } from './format-text'; import { CoreFormatTextDirective } from './format-text';
const defaultMaxHeight = 80; const defaultMaxHeight = 80;
@ -49,7 +50,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
protected maxHeight = defaultMaxHeight; protected maxHeight = defaultMaxHeight;
protected expandedHeight = 0; protected expandedHeight = 0;
protected resizeListener?: CoreEventObserver; protected resizeListener?: CoreEventObserver;
protected domListener?: CoreSingleTimeEventObserver; protected domPromise?: CoreCancellablePromise<void>;
constructor(el: ElementRef<HTMLElement>) { constructor(el: ElementRef<HTMLElement>) {
this.element = el.nativeElement; this.element = el.nativeElement;
@ -96,8 +97,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
* @return Promise resolved when loadings are done. * @return Promise resolved when loadings are done.
*/ */
protected async waitLoadingsDone(): Promise<void> { protected async waitLoadingsDone(): Promise<void> {
this.domListener = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domListener.promise;
await this.domPromise;
const page = this.element.closest('.ion-page'); const page = this.element.closest('.ion-page');
@ -242,7 +244,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.resizeListener?.off(); this.resizeListener?.off();
this.domListener?.off(); this.domPromise?.cancel();
} }
} }

View File

@ -42,7 +42,7 @@ import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreSubscriptions } from '@singletons/subscriptions';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
/** /**
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@ -90,7 +90,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
protected element: HTMLElement; protected element: HTMLElement;
protected emptyText = ''; protected emptyText = '';
protected contentSpan: HTMLElement; protected contentSpan: HTMLElement;
protected domListener?: CoreSingleTimeEventObserver; protected domPromise?: CoreCancellablePromise<void>;
constructor( constructor(
element: ElementRef, element: ElementRef,
@ -133,7 +133,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.domListener?.off(); this.domPromise?.cancel();
} }
/** /**
@ -556,8 +556,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
* @return The width of the element in pixels. * @return The width of the element in pixels.
*/ */
protected async getElementWidth(): Promise<number> { protected async getElementWidth(): Promise<number> {
this.domListener = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domListener.promise;
await this.domPromise;
let width = this.element.getBoundingClientRect().width; let width = this.element.getBoundingClientRect().width;
if (!width) { if (!width) {
@ -711,7 +712,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
let width: string | number; let width: string | number;
let height: string | number; let height: string | number;
await CoreDomUtils.waitToBeInDOM(iframe, 5000).promise; await CoreDomUtils.waitToBeInDOM(iframe, 5000);
if (iframe.width) { if (iframe.width) {
width = iframe.width; width = iframe.width;

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreSingleTimeEventObserver } from '@singletons/events';
/** /**
* Directive to listen when an element becomes visible. * Directive to listen when an element becomes visible.
@ -27,7 +27,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
@Output() onAppear = new EventEmitter(); @Output() onAppear = new EventEmitter();
private element: HTMLElement; private element: HTMLElement;
protected domListener?: CoreSingleTimeEventObserver; protected domPromise?: CoreCancellablePromise<void>;
constructor(element: ElementRef) { constructor(element: ElementRef) {
this.element = element.nativeElement; this.element = element.nativeElement;
@ -37,8 +37,9 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.domListener = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domListener.promise;
await this.domPromise;
this.onAppear.emit(); this.onAppear.emit();
} }
@ -47,7 +48,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.domListener?.off(); this.domPromise?.cancel();
} }
} }

View File

@ -36,11 +36,12 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Platform, Translate } from '@singletons'; import { Platform, Translate } from '@singletons';
import { CoreEventFormActionData, CoreEventObserver, CoreEvents, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreEditorOffline } from '../../services/editor-offline'; import { CoreEditorOffline } from '../../services/editor-offline';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
/** /**
* Component to display a rich text editor if enabled. * Component to display a rich text editor if enabled.
@ -105,7 +106,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
protected selectionChangeFunction?: () => void; protected selectionChangeFunction?: () => void;
protected languageChangedSubscription?: Subscription; protected languageChangedSubscription?: Subscription;
protected resizeListener?: CoreEventObserver; protected resizeListener?: CoreEventObserver;
protected domListener?: CoreSingleTimeEventObserver; protected domPromise?: CoreCancellablePromise<void>;
rteEnabled = false; rteEnabled = false;
isPhone = false; isPhone = false;
@ -288,8 +289,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
* @return Promise resolved when loadings are done. * @return Promise resolved when loadings are done.
*/ */
protected async waitLoadingsDone(): Promise<void> { protected async waitLoadingsDone(): Promise<void> {
this.domListener = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
await this.domListener.promise;
await this.domPromise;
const page = this.element.closest('.ion-page'); const page = this.element.closest('.ion-page');
@ -829,7 +831,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
const length = await this.toolbarSlides.length(); const length = await this.toolbarSlides.length();
await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement, 5000).promise; await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement, 5000);
const width = this.toolbar.nativeElement.getBoundingClientRect().width; const width = this.toolbar.nativeElement.getBoundingClientRect().width;
@ -1089,7 +1091,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
this.keyboardObserver?.off(); this.keyboardObserver?.off();
this.labelObserver?.disconnect(); this.labelObserver?.disconnect();
this.resizeListener?.off(); this.resizeListener?.off();
this.domListener?.off(); this.domPromise?.cancel();
} }
} }

View File

@ -53,7 +53,8 @@ import { NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
/* /*
* "Utils" service with helper functions for UI, DOM elements and HTML code. * "Utils" service with helper functions for UI, DOM elements and HTML code.
@ -97,51 +98,46 @@ export class CoreDomUtilsProvider {
* *
* @param element Element to wait. * @param element Element to wait.
* @param timeout If defined, timeout to wait before rejecting the promise. * @param timeout If defined, timeout to wait before rejecting the promise.
* @return Promise CoreSingleTimeEventObserver with a promise. * @return Cancellable promise.
*/ */
waitToBeInDOM( waitToBeInDOM(element: Element, timeout?: number): CoreCancellablePromise<void> {
element: Element, const root = element.getRootNode({ composed: true });
timeout?: number,
): CoreSingleTimeEventObserver {
let root = element.getRootNode({ composed: true });
if (root === document) { if (root === document) {
// Already in DOM. // Already in DOM.
return { return CoreCancellablePromise.resolve();
off: (): void => {
// Nothing to do here.
},
promise: Promise.resolve(),
};
} }
let observer: MutationObserver | undefined; let observer: MutationObserver;
let observerTimeout: number | undefined; let observerTimeout: number | undefined;
if (timeout) {
observerTimeout = window.setTimeout(() => {
observer?.disconnect();
throw new Error('Waiting for DOM timeout reached');
}, timeout);
}
return { return new CoreCancellablePromise<void>(
off: (): void => { (resolve, reject) => {
observer?.disconnect();
clearTimeout(observerTimeout);
},
promise: new Promise((resolve) => {
observer = new MutationObserver(() => { observer = new MutationObserver(() => {
root = element.getRootNode({ composed: true }); const root = element.getRootNode({ composed: true });
if (root === document) { if (root === document) {
observer?.disconnect(); observer?.disconnect();
clearTimeout(observerTimeout); observerTimeout && clearTimeout(observerTimeout);
resolve(); resolve();
} }
}); });
if (timeout) {
observerTimeout = window.setTimeout(() => {
observer?.disconnect();
reject(new Error('Waiting for DOM timeout reached'));
}, timeout);
}
observer.observe(document.body, { subtree: true, childList: true }); observer.observe(document.body, { subtree: true, childList: true });
}), },
}; () => {
observer?.disconnect();
observerTimeout && clearTimeout(observerTimeout);
},
);
} }
/** /**

View File

@ -31,21 +31,6 @@ export interface CoreEventObserver {
off: () => void; off: () => void;
} }
/**
* Observer instance to stop listening to an observer.
*/
export interface CoreSingleTimeEventObserver {
/**
* Stop the observer.
*/
off: () => void;
/**
* Promise Resolved when event is done (first time).
*/
promise: Promise<void>;
}
/** /**
* Event payloads. * Event payloads.
*/ */