11import _ from 'lodash'
22import { EntryPoint , ObservableState } from '../src/API'
33import { AnySlotKey , AppHost , createAppHost as _createAppHost , EntryPointOrPackage , Shell , SlotKey } from '../src/index'
4+ import { UnreadyEntryPointsReport } from '../src/repluggableAppDebug'
45import { createShellLogger } from '../src/loggers'
56import { emptyLoggerOptions } from './emptyLoggerOptions'
67
@@ -81,34 +82,79 @@ export function createAppHostWithPacts(packages: EntryPointOrPackage[], pacts: P
8182 } )
8283}
8384
84- export async function createAppHostAndWaitForLoading ( packages : EntryPointOrPackage [ ] , pacts : PactAPIBase [ ] ) : Promise < AppHost > {
85+ export interface WaitForLoadingOptions {
86+ /** milliseconds to wait for host loading before rejecting with an unready entry points report (default: 3000) */
87+ timeout ?: number
88+ }
89+
90+ export class AppHostLoadingTimeoutError extends Error {
91+ constructor ( message : string , public readonly report : UnreadyEntryPointsReport ) {
92+ super ( message )
93+ this . name = 'AppHostLoadingTimeoutError'
94+ }
95+ }
96+
97+ const WAIT_FOR_LOADING_PROBE_SHELL = 'Depends on all declared APIs'
98+
99+ const formatUnreadyReport = ( timeout : number , report : UnreadyEntryPointsReport ) : string => {
100+ const { utils } = globalThis . repluggableAppDebug
101+ const isProbeShell = ( name : string ) => name === WAIT_FOR_LOADING_PROBE_SHELL
102+ const header = `createAppHostAndWaitForLoading - host not ready within ${ timeout } ms.`
103+ const unreadyEntryPoints = report . unreadyEntryPoints . filter ( entryPoint => ! isProbeShell ( entryPoint . name ) )
104+ const rootUnreadyEntryPoints = report . rootUnreadyEntryPoints . filter ( entryPoint => ! isProbeShell ( entryPoint . name ) )
105+
106+ if ( report . probableDependencyCycle ) {
107+ const names = unreadyEntryPoints . map ( entryPoint => `"${ entryPoint . name } "` ) . join ( ', ' )
108+ return `${ header }
109+ No root unready API found while ${ unreadyEntryPoints . length } entry points are unready - probable dependency cycle between: ${ names } `
110+ }
111+
112+ const roots = report . rootUnreadyAPIs . map ( ( info , index ) => {
113+ const declaredBy =
114+ info . declaredBy === 'nobody'
115+ ? 'declared by NO entry point (missing package or pact?)'
116+ : `declared by "${ info . declaredBy . entryPointName } " (installed, but its contribution never completed - hung async contributeAPI?)`
117+ const requiredBy = info . requiredBy . filter ( name => ! isProbeShell ( name ) )
118+ const lines = [ `${ index + 1 } . "${ info . key . name } " - ${ declaredBy } ` ]
119+ if ( requiredBy . length ) {
120+ lines . push ( ` required by: ${ requiredBy . join ( ', ' ) } ` )
121+ const tree = utils . visualizeDependencyTree ( utils . traceAPIDependency ( requiredBy [ 0 ] , info . key . name ) )
122+ lines . push ( ...tree . split ( '\n' ) . map ( line => ` ${ line } ` ) )
123+ }
124+ return lines . join ( '\n' )
125+ } )
126+
127+ return `${ header }
128+
129+ Root unready APIs (${ report . rootUnreadyAPIs . length } ):
130+ ${ roots . join ( '\n' ) }
131+
132+ Unready entry points: ${ rootUnreadyEntryPoints . length } blocked directly on root APIs, ${ unreadyEntryPoints . length } total.`
133+ }
134+
135+ export async function createAppHostAndWaitForLoading (
136+ packages : EntryPointOrPackage [ ] ,
137+ pacts : PactAPIBase [ ] ,
138+ { timeout = 3000 } : WaitForLoadingOptions = { }
139+ ) : Promise < AppHost > {
85140 const appHost = createAppHostWithPacts ( packages , pacts )
86141 const declaredAPIs = _ ( packages )
87142 . flatten ( )
88143 . value ( )
89144 . flatMap ( ( entryPoint : EntryPoint ) => ( entryPoint . declareAPIs ? entryPoint . declareAPIs ( ) : [ ] ) )
90145
91- const timeoutPromise = new Promise ( ( resolve , reject ) => {
92- setTimeout ( ( ) => {
93- const readyAPIs = Array . from ( globalThis . repluggableAppDebug . readyAPIs )
94- const unreadyAPIs = declaredAPIs . filter ( api => ! readyAPIs . some ( readyAPI => readyAPI . name === api . name ) )
95- const rootUnreadyAPI = globalThis . repluggableAppDebug . utils . getRootUnreadyAPI ( )
96-
97- reject (
98- new Error (
99- `createAppHostAndWaitForLoading - waiting for loading timed out.
100- there's a high chance this missing API is the main reason for it: ${ JSON . stringify ( rootUnreadyAPI ) }
101-
102- in addition here's the full list of declared APIs that have not been contributed: ${ JSON . stringify ( unreadyAPIs ) } `
103- )
104- )
105- } , 3000 )
146+ let timer : ReturnType < typeof setTimeout > | undefined
147+ const timeoutPromise = new Promise < never > ( ( resolve , reject ) => {
148+ timer = setTimeout ( ( ) => {
149+ const report = globalThis . repluggableAppDebug . utils . getUnreadyEntryPointsReport ( )
150+ reject ( new AppHostLoadingTimeoutError ( formatUnreadyReport ( timeout , report ) , report ) )
151+ } , timeout )
106152 } )
107153
108154 const loadingPromise = new Promise < void > ( async resolve => {
109155 await appHost . addShells ( [
110156 {
111- name : 'Depends on all declared APIs' ,
157+ name : WAIT_FOR_LOADING_PROBE_SHELL ,
112158 getDependencyAPIs ( ) {
113159 return declaredAPIs
114160 } ,
@@ -119,7 +165,14 @@ in addition here's the full list of declared APIs that have not been contributed
119165 ] )
120166 } )
121167
122- return Promise . race ( [ timeoutPromise , loadingPromise ] ) . then ( ( ) => appHost )
168+ try {
169+ await Promise . race ( [ timeoutPromise , loadingPromise ] )
170+ return appHost
171+ } finally {
172+ if ( timer ) {
173+ clearTimeout ( timer )
174+ }
175+ }
123176}
124177
125178type Omit < T , K extends keyof T > = Pick < T , Exclude < keyof T , K > >
0 commit comments