@@ -4,6 +4,7 @@ import { AddressInfo } from "net";
4
4
import open from "open" ;
5
5
import path from "path" ;
6
6
import { fileURLToPath } from "url" ;
7
+ import net from "net" ;
7
8
8
9
const __filename = fileURLToPath ( import . meta. url ) ;
9
10
const __dirname = path . dirname ( __filename ) ;
@@ -14,102 +15,129 @@ export interface CallbackResponse {
14
15
15
16
export interface CallbackServerConfig {
16
17
initialData ?: any ;
18
+ timeout ?: number ;
17
19
}
18
20
19
- class CallbackServer {
20
- private static instance : CallbackServer ;
21
+ export class CallbackServer {
21
22
private server : Server | null = null ;
22
- private app : express . Express ;
23
+ private app = express ( ) ;
23
24
private port : number ;
24
- private previewerPath : string ;
25
- private responseHandlers : Map <
26
- string ,
27
- { resolve : ( data : CallbackResponse ) => void ; initialData ?: any }
28
- > = new Map ( ) ;
25
+ private sessionId = Math . random ( ) . toString ( 36 ) . substring ( 7 ) ;
26
+ private timeoutId ?: NodeJS . Timeout ;
29
27
30
- private constructor ( port = 3333 ) {
28
+ constructor ( port = 3333 ) {
31
29
this . port = port ;
32
- this . app = express ( ) ;
33
- this . previewerPath = path . join ( __dirname , "../../previewer" ) ;
34
- this . setupServer ( ) ;
30
+ this . setupRoutes ( ) ;
35
31
}
36
32
37
- static getInstance ( port ?: number ) : CallbackServer {
38
- if ( ! CallbackServer . instance ) {
39
- CallbackServer . instance = new CallbackServer ( port ) ;
40
- }
41
- return CallbackServer . instance ;
42
- }
33
+ private setupRoutes ( ) {
34
+ const previewerPath = path . join ( __dirname , "../previewer" ) ;
43
35
44
- private setupServer ( ) {
45
36
this . app . use ( express . json ( ) ) ;
46
- this . app . use ( express . static ( this . previewerPath ) ) ;
47
- // app.use(
48
- // cors({
49
- // origin: "*",
50
- // methods: ["GET", "POST", "OPTIONS"],
51
- // allowedHeaders: ["Content-Type"],
52
- // })
53
- // );
37
+ this . app . use ( express . static ( previewerPath ) ) ;
54
38
55
39
this . app . get ( "/callback/:id" , ( req , res ) => {
56
40
const { id } = req . params ;
57
- const initialData = this . responseHandlers . get ( id ) ?. initialData ;
58
- res . json ( { status : "success" , data : initialData } ) ;
41
+ if ( id === this . sessionId ) {
42
+ res . json ( { status : "success" , data : this . config ?. initialData } ) ;
43
+ } else {
44
+ res . status ( 404 ) . json ( { status : "error" , message : "Session not found" } ) ;
45
+ }
59
46
} ) ;
60
47
61
48
this . app . post ( "/callback/:id" , ( req , res ) => {
62
49
const { id } = req . params ;
63
- const handler = this . responseHandlers . get ( id ) ;
50
+ if ( id === this . sessionId && this . promiseResolve ) {
51
+ if ( this . timeoutId ) clearTimeout ( this . timeoutId ) ;
64
52
65
- if ( handler ) {
66
- const data = req . body || { } ;
67
- handler . resolve ( { data } ) ;
68
- this . responseHandlers . delete ( id ) ;
53
+ this . promiseResolve ( { data : req . body || { } } ) ;
54
+ this . shutdown ( ) ;
69
55
}
70
56
71
57
res . json ( { status : "success" } ) ;
72
58
} ) ;
73
59
74
60
this . app . get ( "*" , ( req , res ) => {
75
- res . sendFile ( path . join ( this . previewerPath , "index.html" ) ) ;
61
+ res . sendFile ( path . join ( previewerPath , "index.html" ) ) ;
76
62
} ) ;
77
63
}
78
64
79
- async startServer ( ) : Promise < void > {
80
- if ( ! this . server ) {
81
- this . server = this . app . listen ( this . port , "127.0.0.1" , ( ) => {
82
- const address = this . server ?. address ( ) as AddressInfo ;
83
- const previewUrl = `http://127.0.0.1:${ address . port } ` ;
84
- console . log ( `Preview server running at ${ previewUrl } ` ) ;
85
- } ) ;
65
+ private async shutdown ( ) : Promise < void > {
66
+ if ( this . server ) {
67
+ this . server . close ( ) ;
68
+ this . server = null ;
69
+ }
70
+ if ( this . timeoutId ) {
71
+ clearTimeout ( this . timeoutId ) ;
72
+ }
73
+ }
86
74
87
- this . server . on ( "error" , ( error : Error ) => {
88
- console . error ( "Server error:" , error ) ;
89
- } ) ;
75
+ private isPortAvailable ( port : number ) : Promise < boolean > {
76
+ return new Promise ( ( resolve ) => {
77
+ const tester = net
78
+ . createServer ( )
79
+ . once ( "error" , ( ) => resolve ( false ) )
80
+ . once ( "listening" , ( ) => {
81
+ tester . close ( ) ;
82
+ resolve ( true ) ;
83
+ } )
84
+ . listen ( port , "127.0.0.1" ) ;
85
+ } ) ;
86
+ }
87
+
88
+ private async findAvailablePort ( ) : Promise < number > {
89
+ let port = this . port ;
90
+ for ( let attempt = 0 ; attempt < 100 ; attempt ++ ) {
91
+ if ( await this . isPortAvailable ( port ) ) {
92
+ return port ;
93
+ }
94
+ port ++ ;
90
95
}
96
+ throw new Error ( "Unable to find an available port after 100 attempts" ) ;
91
97
}
92
98
99
+ private config ?: CallbackServerConfig ;
100
+ private promiseResolve ?: ( value : CallbackResponse ) => void ;
101
+ private promiseReject ?: ( reason : any ) => void ;
102
+
93
103
async promptUser (
94
104
config : CallbackServerConfig = { }
95
105
) : Promise < CallbackResponse > {
96
- const { initialData = null } = config ;
106
+ const { initialData = null , timeout = 300000 } = config ;
107
+ this . config = config ;
97
108
98
- await this . startServer ( ) ;
99
- const id = Math . random ( ) . toString ( 36 ) . substring ( 7 ) ;
109
+ try {
110
+ // Find available port and start server
111
+ const availablePort = await this . findAvailablePort ( ) ;
112
+ this . server = this . app . listen ( availablePort , "127.0.0.1" ) ;
100
113
101
- return new Promise < CallbackResponse > ( async ( resolve ) => {
102
- this . responseHandlers . set ( id , { resolve, initialData } ) ;
103
-
104
- if ( ! this . server ) {
105
- await this . startServer ( ) ;
106
- }
114
+ this . server . on ( "error" , ( error ) => {
115
+ if ( this . promiseReject ) this . promiseReject ( error ) ;
116
+ } ) ;
107
117
108
- const address = this . server ! . address ( ) as AddressInfo ;
109
- const previewUrl = `http://127.0.0.1:${ address . port } ?id=${ id } ` ;
110
- open ( previewUrl ) ;
111
- } ) ;
118
+ // Create and return promise
119
+ return new Promise < CallbackResponse > ( ( resolve , reject ) => {
120
+ this . promiseResolve = resolve ;
121
+ this . promiseReject = reject ;
122
+
123
+ // Set timeout
124
+ this . timeoutId = setTimeout ( ( ) => {
125
+ resolve ( { data : { timedOut : true } } ) ;
126
+ this . shutdown ( ) ;
127
+ } , timeout ) ;
128
+
129
+ // Open browser
130
+ const address = this . server ! . address ( ) as AddressInfo ;
131
+ const url = `http://127.0.0.1:${ address . port } ?id=${ this . sessionId } ` ;
132
+
133
+ open ( url ) . catch ( ( error ) => {
134
+ reject ( error ) ;
135
+ this . shutdown ( ) ;
136
+ } ) ;
137
+ } ) ;
138
+ } catch ( error ) {
139
+ await this . shutdown ( ) ;
140
+ throw error ;
141
+ }
112
142
}
113
143
}
114
-
115
- export const callbackServer = CallbackServer . getInstance ( ) ;
0 commit comments