@@ -8,21 +8,25 @@ import RootServices from './RootServices.js';
88import ServiceProviderCatalog from './ServiceProviderCatalog.js' ;
99import ServiceProvider from './ServiceProvider.js' ;
1010
11- // Conditional imports for Node.js only
11+ // Conditional imports for Node.js only — loaded lazily to avoid top-level await
12+ // which prevents browser bundlers (esbuild/webpack) from processing this module.
1213let wrapper , CookieJar , DOMParser ;
1314const isNodeEnvironment = typeof window === 'undefined' ;
14-
15- if ( isNodeEnvironment ) {
16- // Node.js imports
17- const cookiejarSupport = await import ( 'axios-cookiejar-support' ) ;
18- wrapper = cookiejarSupport . wrapper ;
19- const toughCookie = await import ( 'tough-cookie' ) ;
20- CookieJar = toughCookie . CookieJar ;
21- const xmldom = await import ( '@xmldom/xmldom' ) ;
22- DOMParser = xmldom . DOMParser ;
23- } else {
24- // Browser: use native DOMParser
25- DOMParser = window . DOMParser ;
15+ let _nodeModulesLoaded = false ;
16+
17+ async function ensureNodeModules ( ) {
18+ if ( _nodeModulesLoaded ) return ;
19+ _nodeModulesLoaded = true ;
20+ if ( isNodeEnvironment ) {
21+ const cookiejarSupport = await import ( 'axios-cookiejar-support' ) ;
22+ wrapper = cookiejarSupport . wrapper ;
23+ const toughCookie = await import ( 'tough-cookie' ) ;
24+ CookieJar = toughCookie . CookieJar ;
25+ const xmldom = await import ( '@xmldom/xmldom' ) ;
26+ DOMParser = xmldom . DOMParser ;
27+ } else {
28+ DOMParser = window . DOMParser ;
29+ }
2630}
2731
2832// Service providers properties
@@ -175,40 +179,33 @@ export default class OSLCClient {
175179 this . password = password ;
176180 this . configuration_context = configuration_context ;
177181 this . ssoCallback = options . ssoCallback ?? null ;
182+ this . _ldmBaseUrl = options . ldmBaseUrl || null ;
178183 this . rootservices = null ;
179184 this . spc = null ;
180185 this . sp = null ;
181186 this . ownerMap = new Map ( ) ;
182187 this . isNodeEnvironment = isNodeEnvironment ;
188+ this . _options = options ;
189+ this . _initialized = false ;
183190
184- if ( isNodeEnvironment ) {
185- this . jar = options . cookieJar ?? new CookieJar ( ) ;
186- }
187-
188- // Create a base configuration
189- // maxRedirects: 0 so the response interceptor can inspect 3xx redirects
190- // for SSO (IdP) detection. Non-IdP redirects are followed manually in the interceptor.
191+ // Create a base axios client — Node.js cookie jar support is applied
192+ // lazily in _ensureInitialized() to avoid top-level await.
191193 const baseConfig = {
192194 timeout : 30000 ,
193195 maxRedirects : 0 ,
194196 headers : {
195197 'Accept' : 'application/rdf+xml, text/turtle;q=0.9, application/ld+json;q=0.8, application/json;q=0.7, application/xml;q=0.6, text/xml;q=0.5' ,
196198 'OSLC-Core-Version' : '2.0'
197199 } ,
198- validateStatus : status => status === 401 || status < 400 // Accept 2xx, 3xx, and 401
200+ validateStatus : status => status === 401 || status < 400
199201 } ;
200202
201- // Configure for Node.js or Browser
202203 if ( isNodeEnvironment ) {
203- // Node.js: use a cookie jar and keep-alive
204204 baseConfig . keepAlive = true ;
205- baseConfig . jar = this . jar ;
206- this . client = wrapper ( axios . create ( baseConfig ) ) ;
207205 } else {
208- // Browser: use withCredentials for automatic cookie handling
209206 baseConfig . withCredentials = true ;
210- this . client = axios . create ( baseConfig ) ;
211207 }
208+ this . client = axios . create ( baseConfig ) ;
212209
213210 // Add the Configuration-Context header if one is given
214211 if ( configuration_context ) {
@@ -231,6 +228,21 @@ export default class OSLCClient {
231228 ) ;
232229 } ;
233230
231+ /**
232+ * Lazily load Node.js modules and upgrade the axios client with cookie jar
233+ * support. Called automatically before the first network request.
234+ */
235+ async _ensureInitialized ( ) {
236+ if ( this . _initialized ) return ;
237+ this . _initialized = true ;
238+ await ensureNodeModules ( ) ;
239+ if ( isNodeEnvironment ) {
240+ this . jar = this . _options . cookieJar ?? new CookieJar ( ) ;
241+ this . client . defaults . jar = this . jar ;
242+ this . client = wrapper ( this . client ) ;
243+ }
244+ }
245+
234246 /**
235247 * Main auth dispatch — inspects response headers/status and routes to the
236248 * appropriate authentication handler. Re-dispatches after successful auth
@@ -671,11 +683,12 @@ export default class OSLCClient {
671683 * @returns an OSLCResource object containing the resource data
672684 */
673685 async getResource ( url , oslc_version = '2.0' , accept = 'application/rdf+xml' ) {
686+ await this . _ensureInitialized ( ) ;
674687 const headers = {
675688 'Accept' : accept ,
676689 'OSLC-Core-Version' : oslc_version
677690 } ;
678-
691+
679692 let response
680693 try {
681694 response = await this . client . get ( url , { headers } ) ;
@@ -724,6 +737,7 @@ export default class OSLCClient {
724737 * @returns an OSLCResource object containing the resource data
725738 */
726739 async getCompactResource ( url , oslc_version = '2.0' , accept = 'application/x-oslc-compact+xml' ) {
740+ await this . _ensureInitialized ( ) ;
727741 const headers = {
728742 'Accept' : accept ,
729743 'OSLC-Core-Version' : oslc_version
@@ -782,6 +796,7 @@ export default class OSLCClient {
782796
783797
784798 async putResource ( resource , eTag = null , oslc_version = '2.0' ) {
799+ await this . _ensureInitialized ( ) ;
785800 const graph = resource . store ;
786801 if ( ! graph ) {
787802 throw new Error ( 'Resource has no data to update' ) ;
@@ -808,6 +823,7 @@ export default class OSLCClient {
808823 }
809824
810825 async createResource ( resourceType , resource , oslc_version = '2.0' ) {
826+ await this . _ensureInitialized ( ) ;
811827 const graph = resource . store ;
812828 if ( ! graph ) {
813829 throw new Error ( 'Resource has no data to create' ) ;
@@ -840,6 +856,7 @@ export default class OSLCClient {
840856 }
841857
842858 async deleteResource ( resource , oslc_version = '2.0' ) {
859+ await this . _ensureInitialized ( ) ;
843860 const graph = resource . store ;
844861 if ( ! graph ) {
845862 throw new Error ( 'Resource has no data to delete' ) ;
@@ -902,6 +919,7 @@ export default class OSLCClient {
902919 }
903920
904921 async queryWithBase ( queryBase , query ) {
922+ await this . _ensureInitialized ( ) ;
905923 const headers = {
906924 'OSLC-Core-Version' : '2.0' ,
907925 'Accept' : 'application/rdf+xml' ,
@@ -943,6 +961,7 @@ export default class OSLCClient {
943961 }
944962
945963 async getOwner ( url ) {
964+ await this . _ensureInitialized ( ) ;
946965 if ( this . ownerMap . has ( url ) ) {
947966 return this . ownerMap . get ( url ) ;
948967 }
@@ -975,6 +994,25 @@ export default class OSLCClient {
975994 return 'Unknown' ;
976995 }
977996
997+ /**
998+ * Get incoming links to the given target resource URLs, returning
999+ * already-inverted triples (with inverseLinkType).
1000+ * Delegates to LDMClient (composition helper).
1001+ *
1002+ * @param {string[] } targetResourceURLs - URLs of resources to find incoming links for
1003+ * @param {string[] } linkTypes - optional filter of link type URIs
1004+ * @returns {Promise<Array<{targetURL: string, inverseLinkType: string, sourceURL: string}>> }
1005+ */
1006+ async getIncomingLinks ( targetResourceURLs , linkTypes = [ ] ) {
1007+ if ( ! this . _ldmBaseUrl ) return [ ] ;
1008+ await this . _ensureInitialized ( ) ;
1009+
1010+ const { default : LDMClient } = await import ( './LDMClient.js' ) ;
1011+ const ldm = new LDMClient ( this , this . _ldmBaseUrl ) ;
1012+ const triples = await ldm . getIncomingLinks ( targetResourceURLs , linkTypes ) ;
1013+ return ldm . invert ( triples ) ;
1014+ }
1015+
9781016 async getQueryBase ( resourceType ) {
9791017 const query = `
9801018 PREFIX oslc: ${ oslc ( ) }
0 commit comments