5
5
import deepEqual from 'fast-deep-equal' ;
6
6
import { Simulate } from "react-dom/test-utils" ;
7
7
import { PlayObject } from "../../core/Atomic.js" ;
8
- import { buildTrackString } from "../../core/StringUtils.js" ;
8
+ import { buildTrackString , truncateStringToLength } from "../../core/StringUtils.js" ;
9
9
10
10
import {
11
11
configPartsToStrongParts , countRegexes ,
@@ -23,6 +23,9 @@ import {
23
23
import { CommonClientConfig } from "./infrastructure/config/client/index.js" ;
24
24
import { CommonSourceConfig } from "./infrastructure/config/source/index.js" ;
25
25
import play = Simulate . play ;
26
+ import { WebhookPayload } from "./infrastructure/config/health/webhooks.js" ;
27
+ import { AuthCheckError , BuildDataError , ConnectionCheckError , PostInitError , TransformRulesError } from "./errors/MSErrors.js" ;
28
+ import { messageWithCauses , messageWithCausesTruncatedDefault } from "../utils/ErrorUtils.js" ;
26
29
27
30
export default abstract class AbstractComponent {
28
31
requiresAuth : boolean = false ;
@@ -47,27 +50,36 @@ export default abstract class AbstractComponent {
47
50
this . config = config ;
48
51
}
49
52
50
- initialize = async ( ) => {
53
+ public abstract notify ( payload : WebhookPayload ) : Promise < void > ;
54
+
55
+ protected abstract getIdentifier ( ) : string ;
56
+
57
+ initialize = async ( options : { force ?: boolean , notify ?: boolean , notifyTitle ?: string } = { } ) => {
58
+
59
+ const { force = false , notify = false , notifyTitle = 'Init Error' } = options ;
60
+
51
61
this . logger . debug ( 'Attempting to initialize...' ) ;
52
62
try {
53
63
this . initializing = true ;
54
64
if ( this . componentLogger === undefined ) {
55
65
await this . buildComponentLogger ( ) ;
56
66
}
57
- await this . buildInitData ( ) ;
67
+ await this . buildInitData ( force ) ;
58
68
this . buildTransformRules ( ) ;
59
- await this . checkConnection ( ) ;
60
- await this . testAuth ( ) ;
69
+ await this . checkConnection ( force ) ;
70
+ await this . testAuth ( force ) ;
61
71
this . logger . info ( 'Fully Initialized!' ) ;
62
72
try {
63
73
await this . postInitialize ( ) ;
64
74
} catch ( e ) {
65
- this . logger . warn ( new Error ( 'Error occurred during post-initialization hook but was caught ' , { cause : e } ) ) ;
75
+ throw new PostInitError ( 'Error occurred during post-initialization hook' , { cause : e } ) ;
66
76
}
67
77
return true ;
68
78
} catch ( e ) {
69
- this . logger . error ( new Error ( 'Initialization failed' , { cause : e } ) ) ;
70
- return false ;
79
+ if ( notify ) {
80
+ await this . notify ( { title : `${ this . getIdentifier ( ) } - ${ notifyTitle } ` , message : truncateStringToLength ( 500 ) ( messageWithCausesTruncatedDefault ( e ) ) , priority : 'error' } ) ;
81
+ }
82
+ throw new Error ( 'Initialization failed' , { cause : e } ) ;
71
83
} finally {
72
84
this . initializing = false ;
73
85
}
@@ -82,9 +94,23 @@ export default abstract class AbstractComponent {
82
94
return ;
83
95
}
84
96
85
- public async buildInitData ( ) {
97
+ tryInitialize = async ( options : { force ?: boolean , notify ?: boolean , notifyTitle ?: string } = { } ) => {
98
+ if ( this . initializing ) {
99
+ throw new Error ( `Already trying to initialize, cannot attempt while an existing initialization attempt is running.` )
100
+ }
101
+ try {
102
+ return await this . initialize ( options ) ;
103
+ } catch ( e ) {
104
+ throw e ;
105
+ }
106
+ }
107
+
108
+ public async buildInitData ( force : boolean = false ) {
86
109
if ( this . buildOK ) {
87
- return ;
110
+ if ( ! force ) {
111
+ return ;
112
+ }
113
+ this . logger . debug ( 'Build OK but step was forced' ) ;
88
114
}
89
115
try {
90
116
const res = await this . doBuildInitData ( ) ;
@@ -101,7 +127,7 @@ export default abstract class AbstractComponent {
101
127
this . buildOK = true ;
102
128
} catch ( e ) {
103
129
this . buildOK = false ;
104
- throw new Error ( 'Building required data for initialization failed' , { cause : e } ) ;
130
+ throw new BuildDataError ( 'Building required data for initialization failed' , { cause : e } ) ;
105
131
}
106
132
}
107
133
@@ -122,13 +148,13 @@ export default abstract class AbstractComponent {
122
148
this . doBuildTransformRules ( ) ;
123
149
} catch ( e ) {
124
150
this . buildOK = false ;
125
- throw new Error ( 'Could not build playTransform rules. Check your configuration is valid.' , { cause : e } ) ;
151
+ throw new TransformRulesError ( 'Could not build playTransform rules. Check your configuration is valid.' , { cause : e } ) ;
126
152
}
127
153
try {
128
154
const ruleCount = countRegexes ( this . transformRules ) ;
129
155
this . regexCache = cacheFunctions ( ruleCount ) ;
130
156
} catch ( e ) {
131
- this . logger . warn ( new Error ( 'Failed to count number of rule regexes for caching but will continue will fallback to 100' , { cause : e } ) ) ;
157
+ this . logger . warn ( new TransformRulesError ( 'Failed to count number of rule regexes for caching but will continue will fallback to 100' , { cause : e } ) ) ;
132
158
}
133
159
}
134
160
@@ -192,7 +218,13 @@ export default abstract class AbstractComponent {
192
218
}
193
219
}
194
220
195
- public async checkConnection ( ) {
221
+ public async checkConnection ( force : boolean = false ) {
222
+ if ( this . connectionOK ) {
223
+ if ( ! force ) {
224
+ return ;
225
+ }
226
+ this . logger . debug ( 'Connection OK but step was forced' )
227
+ }
196
228
try {
197
229
const res = await this . doCheckConnection ( ) ;
198
230
if ( res === undefined ) {
@@ -207,7 +239,7 @@ export default abstract class AbstractComponent {
207
239
this . connectionOK = true ;
208
240
} catch ( e ) {
209
241
this . connectionOK = false ;
210
- throw new Error ( 'Communicating with upstream service failed' , { cause : e } ) ;
242
+ throw new ConnectionCheckError ( 'Communicating with upstream service failed' , { cause : e } ) ;
211
243
}
212
244
}
213
245
@@ -227,15 +259,30 @@ export default abstract class AbstractComponent {
227
259
228
260
canTryAuth = ( ) => this . isUsable ( ) && this . authGated ( ) && this . authFailure !== true
229
261
262
+ canAuthUnattended = ( ) => ! this . authGated || ! this . requiresAuthInteraction || ( this . requiresAuthInteraction && ! this . authFailure ) ;
263
+
230
264
protected doAuthentication = async ( ) : Promise < boolean > => this . authed
231
265
232
266
// default init function, should be overridden if auth stage is required
233
267
testAuth = async ( force : boolean = false ) => {
234
268
if ( ! this . requiresAuth ) {
235
269
return ;
236
270
}
237
- if ( this . authed && ! force ) {
238
- return ;
271
+ if ( this . authed ) {
272
+ if ( ! force ) {
273
+ return ;
274
+ }
275
+ this . logger . debug ( 'Auth OK but step was forced' ) ;
276
+ }
277
+
278
+ if ( this . authFailure ) {
279
+ if ( ! force ) {
280
+ if ( this . requiresAuthInteraction ) {
281
+ throw new AuthCheckError ( 'Authentication failure: Will not retry auth because user interaction is required for authentication' ) ;
282
+ }
283
+ throw new AuthCheckError ( 'Authentication failure: Will not retry auth because authentication previously failed and must be reauthenticated' ) ;
284
+ }
285
+ this . logger . debug ( 'Auth previously failed for non upstream/network reasons but retry is being forced' ) ;
239
286
}
240
287
241
288
try {
@@ -245,7 +292,7 @@ export default abstract class AbstractComponent {
245
292
// only signal as auth failure if error was NOT either a node network error or a non-showstopping upstream error
246
293
this . authFailure = ! ( hasNodeNetworkException ( e ) || hasUpstreamError ( e , false ) ) ;
247
294
this . authed = false ;
248
- this . logger . error ( new Error ( `Authentication test failed!${ this . authFailure === false ? ' Due to a network issue. Will retry authentication on next heartbeat.' : '' } ` , { cause : e } ) ) ;
295
+ throw new AuthCheckError ( `Authentication test failed!${ this . authFailure === false ? ' Due to a network issue. Will retry authentication on next heartbeat.' : '' } ` , { cause : e } ) ;
249
296
}
250
297
}
251
298
0 commit comments