@@ -18,6 +18,16 @@ export default class Service extends EventEmitter {
18
18
*/
19
19
_serviceCallback = null ;
20
20
isAdvertised = false ;
21
+ /**
22
+ * Queue for serializing advertise/unadvertise operations to prevent race conditions
23
+ * @private
24
+ */
25
+ _operationQueue = Promise . resolve ( ) ;
26
+ /**
27
+ * Track if an unadvertise operation is pending to prevent double operations
28
+ * @private
29
+ */
30
+ _pendingUnadvertise = false ;
21
31
/**
22
32
* @param {Object } options
23
33
* @param {Ros } options.ros - The ROSLIB.Ros connection handle.
@@ -93,84 +103,134 @@ export default class Service extends EventEmitter {
93
103
* @param {advertiseCallback } callback - This works similarly to the callback for a C++ service and should take the following params
94
104
*/
95
105
advertise ( callback ) {
96
- if ( this . isAdvertised ) {
97
- throw new Error ( 'Cannot advertise the same Service twice!' ) ;
98
- }
106
+ // Queue this operation to prevent race conditions
107
+ this . _operationQueue = this . _operationQueue . then ( async ( ) => {
108
+ // If already advertised, unadvertise first
109
+ if ( this . isAdvertised ) {
110
+ await this . _doUnadvertise ( ) ;
111
+ }
99
112
100
- // Store the new callback for removal during un-advertisement
101
- this . _serviceCallback = ( rosbridgeRequest ) => {
102
- var response = { } ;
103
- var success = callback ( rosbridgeRequest . args , response ) ;
113
+ // Store the new callback for removal during un-advertisement
114
+ this . _serviceCallback = ( rosbridgeRequest ) => {
115
+ var response = { } ;
116
+ var success = callback ( rosbridgeRequest . args , response ) ;
104
117
105
- var call = {
106
- op : 'service_response' ,
107
- service : this . name ,
108
- values : response ,
109
- result : success
110
- } ;
118
+ var call = {
119
+ op : 'service_response' ,
120
+ service : this . name ,
121
+ values : response ,
122
+ result : success
123
+ } ;
111
124
112
- if ( rosbridgeRequest . id ) {
113
- call . id = rosbridgeRequest . id ;
114
- }
125
+ if ( rosbridgeRequest . id ) {
126
+ call . id = rosbridgeRequest . id ;
127
+ }
115
128
116
- this . ros . callOnConnection ( call ) ;
117
- } ;
129
+ this . ros . callOnConnection ( call ) ;
130
+ } ;
118
131
119
- this . ros . on ( this . name , this . _serviceCallback ) ;
120
- this . ros . callOnConnection ( {
121
- op : 'advertise_service' ,
122
- type : this . serviceType ,
123
- service : this . name
132
+ this . ros . on ( this . name , this . _serviceCallback ) ;
133
+ this . ros . callOnConnection ( {
134
+ op : 'advertise_service' ,
135
+ type : this . serviceType ,
136
+ service : this . name
137
+ } ) ;
138
+ this . isAdvertised = true ;
139
+ } ) . catch ( err => {
140
+ this . emit ( 'error' , err ) ;
141
+ throw err ;
124
142
} ) ;
125
- this . isAdvertised = true ;
143
+
144
+ return this . _operationQueue ;
126
145
}
127
146
128
- unadvertise ( ) {
129
- if ( ! this . isAdvertised ) {
130
- throw new Error ( `Tried to un-advertise service ${ this . name } , but it was not advertised!` ) ;
147
+ /**
148
+ * Internal method to perform unadvertisement without queueing
149
+ * @private
150
+ */
151
+ async _doUnadvertise ( ) {
152
+ if ( ! this . isAdvertised || this . _pendingUnadvertise ) {
153
+ return ;
131
154
}
132
- this . ros . callOnConnection ( {
133
- op : 'unadvertise_service' ,
134
- service : this . name
135
- } ) ;
136
- // Remove the registered callback
137
- if ( this . _serviceCallback ) {
138
- this . ros . off ( this . name , this . _serviceCallback ) ;
155
+
156
+ this . _pendingUnadvertise = true ;
157
+
158
+ try {
159
+ // Mark as not advertised first to prevent new service calls
160
+ // This ensures callService() will not be blocked while we're unadvertising
161
+ this . isAdvertised = false ;
162
+
163
+ // Remove the registered callback to stop processing new requests
164
+ if ( this . _serviceCallback ) {
165
+ this . ros . off ( this . name , this . _serviceCallback ) ;
166
+ this . _serviceCallback = null ;
167
+ }
168
+
169
+ // Send the unadvertise message to the server
170
+ // Note: This is fire-and-forget, but the operation queue ensures
171
+ // no new advertise can start until this completes
172
+ this . ros . callOnConnection ( {
173
+ op : 'unadvertise_service' ,
174
+ service : this . name
175
+ } ) ;
176
+ } finally {
177
+ this . _pendingUnadvertise = false ;
139
178
}
140
- this . isAdvertised = false ;
179
+ }
180
+
181
+ unadvertise ( ) {
182
+ // Queue this operation to prevent race conditions
183
+ this . _operationQueue = this . _operationQueue . then ( async ( ) => {
184
+ await this . _doUnadvertise ( ) ;
185
+ } ) . catch ( err => {
186
+ this . emit ( 'error' , err ) ;
187
+ throw err ;
188
+ } ) ;
189
+
190
+ return this . _operationQueue ;
141
191
}
142
192
143
193
/**
144
194
* An alternate form of Service advertisement that supports a modern Promise-based interface for use with async/await.
145
195
* @param {(request: TRequest) => Promise<TResponse> } callback An asynchronous callback processing the request and returning a response.
146
196
*/
147
197
advertiseAsync ( callback ) {
148
- if ( this . isAdvertised ) {
149
- throw new Error ( 'Cannot advertise the same Service twice!' ) ;
150
- }
151
- this . _serviceCallback = async ( rosbridgeRequest ) => {
152
- /** @type {{op: string, service: string, values?: TResponse, result: boolean, id?: string} } */
153
- let rosbridgeResponse = {
154
- op : 'service_response' ,
155
- service : this . name ,
156
- result : false
198
+ // Queue this operation to prevent race conditions
199
+ this . _operationQueue = this . _operationQueue . then ( async ( ) => {
200
+ // If already advertised, unadvertise first
201
+ if ( this . isAdvertised ) {
202
+ await this . _doUnadvertise ( ) ;
157
203
}
158
- try {
159
- rosbridgeResponse . values = await callback ( rosbridgeRequest . args ) ;
160
- rosbridgeResponse . result = true ;
161
- } finally {
162
- if ( rosbridgeRequest . id ) {
163
- rosbridgeResponse . id = rosbridgeRequest . id ;
204
+
205
+ this . _serviceCallback = async ( rosbridgeRequest ) => {
206
+ /** @type {{op: string, service: string, values?: TResponse, result: boolean, id?: string} } */
207
+ let rosbridgeResponse = {
208
+ op : 'service_response' ,
209
+ service : this . name ,
210
+ result : false
211
+ }
212
+ try {
213
+ rosbridgeResponse . values = await callback ( rosbridgeRequest . args ) ;
214
+ rosbridgeResponse . result = true ;
215
+ } finally {
216
+ if ( rosbridgeRequest . id ) {
217
+ rosbridgeResponse . id = rosbridgeRequest . id ;
218
+ }
219
+ this . ros . callOnConnection ( rosbridgeResponse ) ;
164
220
}
165
- this . ros . callOnConnection ( rosbridgeResponse ) ;
166
221
}
167
- }
168
- this . ros . on ( this . name , this . _serviceCallback ) ;
169
- this . ros . callOnConnection ( {
170
- op : 'advertise_service' ,
171
- type : this . serviceType ,
172
- service : this . name
222
+ this . ros . on ( this . name , this . _serviceCallback ) ;
223
+ this . ros . callOnConnection ( {
224
+ op : 'advertise_service' ,
225
+ type : this . serviceType ,
226
+ service : this . name
227
+ } ) ;
228
+ this . isAdvertised = true ;
229
+ } ) . catch ( err => {
230
+ this . emit ( 'error' , err ) ;
231
+ throw err ;
173
232
} ) ;
174
- this . isAdvertised = true ;
233
+
234
+ return this . _operationQueue ;
175
235
}
176
236
}
0 commit comments