@@ -11,8 +11,12 @@ export interface Message {
1111export interface Response {
1212 response_to : string ;
1313 data : { [ key : string ] : unknown } ;
14+ error ?: string ;
1415}
1516
17+ const browserApi = ( globalThis as typeof globalThis & { browser ?: typeof chrome } ) . browser ;
18+ const runtimeApi = browserApi ?. runtime ?? chrome . runtime ;
19+
1620function createRandomString ( length : number = 16 ) {
1721 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ;
1822 let result = "" ;
@@ -25,60 +29,121 @@ function createRandomString(length: number = 16) {
2529}
2630
2731const defaultReconnectDelay = 5 ;
32+ const requestTimeoutMs = 2500 ;
33+
34+ type PendingRequest = PromiseWithResolvers < Response > & {
35+ timeout ?: ReturnType < typeof setTimeout > ;
36+ } ;
2837
2938export class Native {
3039 #port?: chrome . runtime . Port ;
31- #promises: Map < string , PromiseWithResolvers < Response > > = new Map ( ) ;
40+ #promises: Map < string , PendingRequest > = new Map ( ) ;
3241 #reconnectDelay = defaultReconnectDelay ;
3342 #reconnectTimeout = 0 ;
43+ #isConnected = false ;
3444
3545 constructor ( ) {
3646 this . #connect( ) ;
3747 }
3848
3949 #connect( ) {
40- this . #port = chrome . runtime . connectNative ( "io.goauthentik.platform" ) ;
41- this . #port. onMessage . addListener ( this . #listener. bind ( this ) ) ;
42- this . #port. onDisconnect . addListener ( ( ) => {
50+ const port = runtimeApi . connectNative ( "io.goauthentik.platform" ) ;
51+ this . #port = port ;
52+ this . #isConnected = true ;
53+ this . #reconnectDelay = defaultReconnectDelay ;
54+ port . onMessage . addListener ( this . #listener. bind ( this ) ) ;
55+ port . onDisconnect . addListener ( ( ) => {
56+ this . #isConnected = false ;
4357 this . #reconnectDelay *= 1.35 ;
4458 this . #reconnectDelay = Math . min ( this . #reconnectDelay, 3600 ) ;
45- // @ts -ignore
46- const err = chrome . runtime . lastError || this . #port ?. error ;
47- console . debug (
48- `authentik/bext/native: Disconnected, reconnecting in ${ this . #reconnectDelay } ` ,
49- err ,
59+ const err =
60+ ( typeof chrome !== "undefined" ? chrome . runtime ? .lastError : undefined ) ||
61+ ( port as chrome . runtime . Port & { error ?: unknown } ) . error ;
62+ this . #rejectPending (
63+ new Error ( `native host disconnected ${ err ? `: ${ String ( err ) } ` : "" } ` ) ,
5064 ) ;
65+ this . #port = undefined ;
5166 clearTimeout ( this . #reconnectTimeout) ;
5267 this . #reconnectTimeout = setTimeout ( ( ) => {
5368 this . #connect( ) ;
5469 } , this . #reconnectDelay * 1000 ) ;
5570 } ) ;
56- console . debug ( "authentik/bext/native: Connected to native" ) ;
5771 }
5872
5973 #listener( msg : Response ) {
6074 const prom = this . #promises. get ( msg . response_to ) ;
61- console . debug ( `authentik/bext/native[${ msg . response_to } ]: Got response` ) ;
6275 if ( ! prom ) {
63- console . debug ( `authentik/bext/native[${ msg . response_to } ]: No promise to resolve` ) ;
6476 return ;
6577 }
78+ if ( msg . error ) {
79+ if ( prom . timeout ) {
80+ clearTimeout ( prom . timeout ) ;
81+ }
82+ prom . reject ( new Error ( msg . error ) ) ;
83+ this . #promises. delete ( msg . response_to ) ;
84+ return ;
85+ }
86+ if ( prom . timeout ) {
87+ clearTimeout ( prom . timeout ) ;
88+ }
6689 prom . resolve ( msg ) ;
90+ this . #promises. delete ( msg . response_to ) ;
91+ }
92+
93+ #postMessage( msg : Message , retry : boolean ) {
94+ if ( ! this . #port || ! this . #isConnected) {
95+ this . #connect( ) ;
96+ }
97+ if ( ! this . #port) {
98+ throw new Error ( "native host is not connected" ) ;
99+ }
100+ try {
101+ this . #port. postMessage ( msg ) ;
102+ } catch ( exc ) {
103+ const err = exc instanceof Error ? exc . message : String ( exc ) ;
104+ if ( retry && err . includes ( "disconnected port" ) ) {
105+ this . #isConnected = false ;
106+ this . #port = undefined ;
107+ this . #connect( ) ;
108+ this . #postMessage( msg , false ) ;
109+ return ;
110+ }
111+ throw exc ;
112+ }
67113 }
68114
69115 postMessage ( msg : Partial < Message > ) : Promise < Response > {
70116 msg . id = createRandomString ( ) ;
71117 const promise = Promise . withResolvers < Response > ( ) ;
72118 try {
73- this . #promises. set ( msg . id , promise ) ;
74- this . #port?. postMessage ( msg ) ;
75- console . debug ( `authentik/bext/native[${ msg . id } ]: Sending message ${ msg . path } ` ) ;
119+ const pending = promise as PendingRequest ;
120+ pending . timeout = setTimeout ( ( ) => {
121+ this . #promises. delete ( msg . id as string ) ;
122+ pending . reject ( new Error ( `native host timed out after ${ requestTimeoutMs } ms` ) ) ;
123+ } , requestTimeoutMs ) ;
124+ this . #promises. set ( msg . id , pending ) ;
125+ this . #postMessage( msg as Message , true ) ;
76126 } catch ( exc ) {
77- this . #promises. get ( msg . id ) ?. reject ( exc ) ;
127+ const pending = this . #promises. get ( msg . id ) ;
128+ if ( pending ?. timeout ) {
129+ clearTimeout ( pending . timeout ) ;
130+ }
131+ pending ?. reject ( exc ) ;
132+ this . #promises. delete ( msg . id ) ;
78133 }
79134 return promise . promise ;
80135 }
81136
137+ #rejectPending( error : Error ) {
138+ for ( const [ id , pending ] of this . #promises) {
139+ if ( pending . timeout ) {
140+ clearTimeout ( pending . timeout ) ;
141+ }
142+ pending . reject ( error ) ;
143+ this . #promises. delete ( id ) ;
144+ }
145+ }
146+
82147 async ping ( ) : Promise < Response > {
83148 return this . postMessage ( {
84149 version : "1" ,
0 commit comments