@@ -34,6 +34,15 @@ export type CloudCapabilitySandboxProvisionResult = {
3434 jobId ?: string ;
3535} ;
3636
37+ export type WaitForCloudCapabilityEndpointAvailabilityOptions = {
38+ endpoint : RemoteCapabilityEndpointConfig ;
39+ timeoutMs ?: number ;
40+ pollIntervalMs ?: number ;
41+ requestTimeoutMs ?: number ;
42+ fetch ?: typeof fetch ;
43+ onProgress ?: ( detail : string ) => void ;
44+ } ;
45+
3746export type ConnectCloudCapabilitySandboxOptions =
3847 CloudCapabilitySandboxProvisionOptions & {
3948 unloadMissing ?: boolean ;
@@ -246,6 +255,99 @@ export async function connectCloudCapabilitySandbox(
246255 } ;
247256}
248257
258+ export async function waitForCloudCapabilityEndpointAvailability (
259+ options : WaitForCloudCapabilityEndpointAvailabilityOptions ,
260+ ) : Promise < void > {
261+ const request = options . fetch ?? fetch ;
262+ const timeoutMs = options . timeoutMs ?? 120_000 ;
263+ const pollIntervalMs = options . pollIntervalMs ?? 5_000 ;
264+ const requestTimeoutMs = options . requestTimeoutMs ?? 60_000 ;
265+ const deadline = Date . now ( ) + timeoutMs ;
266+ let lastError = "availability was not checked" ;
267+
268+ while ( Date . now ( ) < deadline ) {
269+ try {
270+ const controller = new AbortController ( ) ;
271+ const timeout = setTimeout ( ( ) => controller . abort ( ) , requestTimeoutMs ) ;
272+ try {
273+ const response = await request (
274+ new URL ( "/v1/capabilities" , options . endpoint . baseUrl ) ,
275+ {
276+ method : "GET" ,
277+ headers : {
278+ accept : "application/json" ,
279+ ...( options . endpoint . token
280+ ? { authorization : `Bearer ${ options . endpoint . token } ` }
281+ : { } ) ,
282+ } ,
283+ signal : controller . signal ,
284+ } ,
285+ ) ;
286+ const text = await response . text ( ) ;
287+ if ( ! response . ok ) {
288+ lastError = `HTTP ${ response . status } : ${ text . slice ( 0 , 500 ) } ` ;
289+ } else {
290+ const availability = JSON . parse ( text ) as {
291+ available ?: unknown ;
292+ capabilities ?: { plugin ?: unknown } ;
293+ } ;
294+ if (
295+ availability . available === true &&
296+ availability . capabilities ?. plugin === true
297+ ) {
298+ return ;
299+ }
300+ lastError = `unexpected availability payload: ${ text . slice ( 0 , 500 ) } ` ;
301+ }
302+ } finally {
303+ clearTimeout ( timeout ) ;
304+ }
305+ } catch ( error ) {
306+ lastError = describeAvailabilityError ( error ) ;
307+ }
308+ options . onProgress ?.(
309+ `waiting for endpoint ${ options . endpoint . id } : ${ lastError } ` ,
310+ ) ;
311+ await sleep ( pollIntervalMs ) ;
312+ }
313+
314+ throw new Error (
315+ `Cloud capability endpoint ${ options . endpoint . id } did not report plugin availability within ${ timeoutMs } ms. Last error: ${ lastError } ` ,
316+ ) ;
317+ }
318+
319+ function describeAvailabilityError ( error : unknown ) : string {
320+ if ( ! ( error instanceof Error ) ) return String ( error ) ;
321+ const cause = ( error as Error & { cause ?: unknown } ) . cause ;
322+ if ( cause instanceof Error ) {
323+ return `${ error . message } : ${ cause . message } ` ;
324+ }
325+ if ( cause && typeof cause === "object" ) {
326+ const detail = cause as {
327+ code ?: unknown ;
328+ errno ?: unknown ;
329+ syscall ?: unknown ;
330+ hostname ?: unknown ;
331+ address ?: unknown ;
332+ port ?: unknown ;
333+ } ;
334+ const parts = [
335+ detail . code ,
336+ detail . errno ,
337+ detail . syscall ,
338+ detail . hostname ,
339+ detail . address ,
340+ detail . port ,
341+ ]
342+ . filter ( ( value ) => typeof value === "string" || typeof value === "number" )
343+ . map ( String ) ;
344+ if ( parts . length > 0 ) {
345+ return `${ error . message } : ${ parts . join ( " " ) } ` ;
346+ }
347+ }
348+ return error . message ;
349+ }
350+
249351export {
250352 buildRemoteCapabilityEndpointTrustPolicy as buildEndpointTrustPolicy ,
251353 installRemoteCapabilityEndpoint ,
0 commit comments