@@ -8,6 +8,8 @@ import collectAndStorePing from "../pings/maker.js";
88import type CommonPingData from "./common_ping_data.js" ;
99import { Context } from "../context.js" ;
1010
11+ type ValidatorFunction = ( reason ?: string ) => Promise < void > ;
12+
1113/**
1214 * Stores information about a ping.
1315 *
@@ -20,11 +22,22 @@ class PingType implements CommonPingData {
2022 readonly sendIfEmpty : boolean ;
2123 readonly reasonCodes : string [ ] ;
2224
25+ // The functions and promises required for the test API to
26+ // execute and synchronize with the submission API.
27+ private testResolutionFunction ?: ( value : void | PromiseLike < void > ) => void ;
28+ private testValidator ?: ValidatorFunction ;
29+ private testValidatorPromise : Promise < void > ;
30+
2331 constructor ( meta : CommonPingData ) {
2432 this . name = meta . name ;
2533 this . includeClientId = meta . includeClientId ;
2634 this . sendIfEmpty = meta . sendIfEmpty ;
2735 this . reasonCodes = meta . reasonCodes ?? [ ] ;
36+
37+ // This promise should always be resolved to account for the
38+ // fact that no testing API might be called. In production the
39+ // `submit` function will await on this (no-op).
40+ this . testValidatorPromise = Promise . resolve ( ) ;
2841 }
2942
3043 private isDeletionRequest ( ) : boolean {
@@ -44,6 +57,31 @@ class PingType implements CommonPingData {
4457 * `ping_info.reason` part of the payload.
4558 */
4659 submit ( reason ?: string ) : void {
60+ // **** Read this before changing the following code! ****
61+ //
62+ // The Dispatcher does not allow dispatched tasks to await on
63+ // other dispatched tasks. Unfortunately, this causes a deadlock.
64+ // In order to work around that problem, we kick off validation
65+ // right before the actual submission takes place, through another
66+ // async function (and not through the dispatcher). We then await
67+ // in the dispatched submission task on the related promise, which
68+ // will always resolve due to the use of `finally`.
69+ if ( this . testValidator ) {
70+ this . testValidatorPromise = this . testValidator ( reason )
71+ . then ( ( ) => {
72+ if ( this . testResolutionFunction ) {
73+ this . testResolutionFunction ( ) ;
74+ }
75+ } )
76+ . catch (
77+ e => console . error ( `There was an error validating ${ this . name } (${ reason ?? "no reason" } ). Ignoring.` , e )
78+ )
79+ . finally ( ( ) => {
80+ this . testResolutionFunction = undefined ;
81+ this . testValidator = undefined ;
82+ } ) ;
83+ }
84+
4785 Context . dispatcher . launch ( async ( ) => {
4886 if ( ! Context . initialized ) {
4987 console . info ( "Glean must be initialized before submitting pings." ) ;
@@ -60,12 +98,34 @@ class PingType implements CommonPingData {
6098 console . error ( `Invalid reason code ${ reason } from ${ this . name } . Ignoring.` ) ;
6199 correctedReason = undefined ;
62100 }
63-
101+
102+ await this . testValidatorPromise ;
103+
64104 const identifier = generateUUIDv4 ( ) ;
65105 await collectAndStorePing ( identifier , this , correctedReason ) ;
66106 return ;
67107 } ) ;
68108 }
109+
110+ /**
111+ * **Test-only API**
112+ *
113+ * Runs a validation function before the ping is collected.
114+ *
115+ * TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
116+ *
117+ * @param validatorFn The asynchronous validation function to run in order to validate
118+ * the ping content.
119+ *
120+ * @returns A `Promise` resolved when the ping is collected and the validation function
121+ * is executed.
122+ */
123+ async testBeforeNextSubmit ( validatorFn : ValidatorFunction ) : Promise < void > {
124+ return new Promise ( ( resolve ) => {
125+ this . testResolutionFunction = resolve ;
126+ this . testValidator = validatorFn ;
127+ } ) ;
128+ }
69129}
70130
71131export default PingType ;
0 commit comments