1
- use std:: {
2
- cell:: Ref ,
3
- mem,
4
- sync:: { Arc , OnceLock } ,
1
+ use std:: sync:: {
2
+ Arc , OnceLock ,
3
+ atomic:: { AtomicU8 , Ordering , fence} ,
5
4
} ;
6
5
7
6
use neon:: {
@@ -10,7 +9,6 @@ use neon::{
10
9
prelude:: { Context , JsResult } ,
11
10
types:: { JsFunction , JsObject , JsValue } ,
12
11
} ;
13
- use parking_lot:: { Mutex , Once } ;
14
12
15
13
use super :: { BridgeResult , JsCallback , TryIntoJs , errors:: IntoThrow as _} ;
16
14
@@ -25,27 +23,61 @@ pub type JsAbortSignal = JsValue;
25
23
/// The JS counterpart object is lazily instantiated when the signal gets converted to JS (through
26
24
/// the `TryIntoJs` trait); this ensures that the Rust side can be created without a JS `Context`.
27
25
pub struct AbortController {
28
- inner : AbortControllerInner ,
26
+ inner : Arc < AbortControllerInner > ,
29
27
drop_abort_reason : String ,
30
28
}
31
29
32
30
/// An object that models the signal of a JavaScript `AbortController`.
33
31
pub struct AbortSignal {
34
- inner : AbortControllerInner ,
32
+ inner : Arc < AbortControllerInner > ,
35
33
}
36
34
37
- /// The inner state of an `AbortController`, shared between the Rust's controller and its signal.
38
- type AbortControllerInner = Arc < Mutex < AbortControllerInnerState > > ;
35
+ /// The inner state of an `AbortController`, shared between the Rust and JS sides.
36
+ struct AbortControllerInner {
37
+ // The fact that we require a `Context` to initialize the JS counterpart means that we are running
38
+ // on the Node's thread, which guarantees that there can't be multiple threads calling into that
39
+ // function concurrently; that should in theory aleviate the need to use a lock on `js_counterpart`.
40
+ //
41
+ // It is however possible for the rust-side controller to get aborted from a non-Node thread
42
+ // while the JS-side controller is being created on the Node thread, in which case we don't
43
+ // want the Rust-side thread to get blocked for the JS-side to complete instantiation.
44
+ //
45
+ // By modelling the "JS initialization" and "is aborted" states as two distinct independant
46
+ // structures, we ensure that we're never blocking execution of either thread. This however
47
+ // means that either step may happen before the other, so we need to be careful not to miss
48
+ // sending the abort signal. The good news is that nothing bad will happen if we call the JS
49
+ // abort callback multiple times.
50
+ js_counterpart : OnceLock < AbortControllerJsCounterpart > ,
51
+ aborted : OnceLock < String > ,
52
+
53
+ state : AtomicU8 ,
54
+ }
55
+
56
+ struct AbortControllerJsCounterpart {
57
+ controller : Root < JsObject > ,
58
+ abort : JsCallback < ( String , ) , ( ) > ,
59
+ }
60
+
61
+ const STATE_UNINITIALIZED : u8 = 0 ;
62
+ const STATE_ARMED : u8 = 1 ;
63
+ const STATE_ABORTED : u8 = 2 ;
64
+ const STATE_DISARMED : u8 = 3 ;
39
65
40
66
impl AbortController {
41
- /// Create a new `AbortController` and `AbortSignal` pair .
67
+ /// Create a new `AbortController`.
42
68
///
43
69
/// The `drop_abort_reason` string will be used as the reason for the abort
44
70
/// if the controller is dropped from the Rust side.
45
71
#[ must_use]
46
72
pub fn new ( drop_abort_reason : String ) -> Self {
73
+ let inner = AbortControllerInner {
74
+ js_counterpart : OnceLock :: new ( ) ,
75
+ aborted : OnceLock :: new ( ) ,
76
+ state : AtomicU8 :: new ( STATE_UNINITIALIZED ) ,
77
+ } ;
78
+ let inner = Arc :: new ( inner) ;
47
79
Self {
48
- inner : Arc :: new ( Mutex :: new ( AbortControllerInnerState :: Uninitialized ) ) ,
80
+ inner : inner . clone ( ) ,
49
81
drop_abort_reason,
50
82
}
51
83
}
@@ -54,7 +86,7 @@ impl AbortController {
54
86
/// be called at any time (i.e. even after the controller has been cancelled),
55
87
/// any number of times (i.e. to pass a same signal to multiple JS functions),
56
88
/// and from any thread.
57
- pub fn signal ( & self ) -> AbortSignal {
89
+ pub fn get_signal ( & self ) -> AbortSignal {
58
90
AbortSignal {
59
91
inner : self . inner . clone ( ) ,
60
92
}
@@ -65,12 +97,7 @@ impl AbortController {
65
97
/// This method can be called at any time (i.e. even before the controller has been armed,
66
98
/// or after it has been disarmed), and from any thread.
67
99
pub fn abort ( & self , reason : impl Into < String > ) {
68
- let call_abort = self . inner . lock ( ) . abort ( reason) ;
69
- if let Some ( call_abort) = call_abort {
70
- call_abort
71
- . abort_cb
72
- . call_on_js_thread ( ( call_abort. abort_reason , ) ) ;
73
- }
100
+ self . inner . abort ( reason) ;
74
101
}
75
102
76
103
/// Disarm the controller, so that it can no longer be aborted.
@@ -80,7 +107,7 @@ impl AbortController {
80
107
/// on the JS side, e.g. when the called JS function has returned, as this will prevent the
81
108
/// overhead of implicit abortion when the controller is dropped.
82
109
pub fn disarm ( & self ) {
83
- self . inner . lock ( ) . disarm ( ) ;
110
+ self . inner . disarm ( ) ;
84
111
}
85
112
}
86
113
@@ -95,220 +122,89 @@ impl Drop for AbortController {
95
122
impl TryIntoJs for AbortSignal {
96
123
type Output = JsAbortSignal ;
97
124
fn try_into_js < ' cx > ( self , cx : & mut impl Context < ' cx > ) -> JsResult < ' cx , JsAbortSignal > {
98
- self . inner . get_signal ( cx) . into_throw ( cx)
125
+ let controller = self . inner . get_or_init_controller ( cx) . into_throw ( cx) ?;
126
+ controller. get ( cx, "signal" )
99
127
}
100
128
}
101
129
102
- ////////////////////////////////////////////////////////////////////////////////////////////////////
103
-
104
130
impl AbortControllerInner {
105
131
/// Create the JS `AbortController` if it hasn't been created yet.
106
132
/// Returns a reference to the signal object that can be passed to JS.
107
- fn get_js_controller < ' cx , C : Context < ' cx > > (
133
+ fn get_or_init_controller < ' cx , C : Context < ' cx > > (
108
134
& self ,
109
135
cx : & mut C ,
110
136
) -> BridgeResult < Handle < ' cx , JsAbortController > > {
111
- // Not using `get_or_init` here because initialization of JS classes can theoretically throw.
112
- // But anyway, having a `Context` implies that we are running on the single Node's thread,
113
- // so there's no risk of having two threads racing to initialize the JS `AbortController`.
114
- if let Some ( js_controller) = self . state . get_controller ( ) {
115
- return Ok ( js_controller. to_inner ( cx) ) ;
137
+ if let Some ( js_counterpart) = self . js_counterpart . get ( ) {
138
+ // Already initialized, return the controller
139
+ return Ok ( js_counterpart. controller . to_inner ( cx) . upcast ( ) ) ;
116
140
}
117
141
118
- // Instantiate the JS AbortController counterpart
142
+ // Not initialized yet, create the JS AbortController
119
143
let global = cx. global_object ( ) ;
120
144
let abort_controller_class = global. get :: < JsFunction , _ , _ > ( cx, "AbortController" ) ?;
121
145
let js_controller = abort_controller_class. construct ( cx, [ ] ) ?;
122
146
123
- // Get the JS `abort` method and make a callback out of it
124
147
let abort_fn = js_controller. get :: < JsFunction , _ , _ > ( cx, "abort" ) ?;
125
148
let abort_cb = JsCallback :: new ( cx, abort_fn, Some ( js_controller) ) ;
126
149
127
- let js_controller = js_controller. root ( cx) ;
128
- let js_controller = match self . js_controller . set ( js_controller) {
129
- Ok ( ( ) ) => {
130
- // We confirmed instantiation of the JS counterpart; now update the state machine
131
- let mut state = self . state . lock ( ) ;
132
- match * state {
133
- AbortControllerInnerState :: Uninitialized => {
134
- * state = AbortControllerInnerState :: Armed ( abort_cb) ;
135
- }
136
- AbortControllerInnerState :: AbortedBeforeArmed ( reason) => {
137
- // Controller was aborted before the JS counterpart was initialized, so we must
138
- // immediately and synchronously propagate the abort to the JS controller object.
139
-
140
- // ... but let's not hold on to the state mutex while calling the abort callback
141
- * state = AbortControllerInnerState :: Disarmed ;
142
- mem:: drop ( state) ;
150
+ let js_counterpart = AbortControllerJsCounterpart {
151
+ controller : js_controller. root ( cx) ,
152
+ abort : abort_cb,
153
+ } ;
143
154
144
- // Call the abort callback
145
- let _ = abort_cb. call ( cx, ( reason, ) ) ;
146
- }
147
- AbortControllerInnerState :: Armed ( _) => {
148
- panic ! (
149
- "AbortController: Incoherent state:JS AbortController already initialized"
150
- ) ;
151
- }
152
- _ => { }
155
+ let controller = match self . js_counterpart . set ( js_counterpart) {
156
+ Ok ( ( ) ) => {
157
+ // Ordering: Write to `state` implies previous write to `js_counterpart` is visible to other threads
158
+ if self . state . fetch_or ( STATE_ARMED , Ordering :: Release ) == STATE_ABORTED {
159
+ // Ordering: Previous concurrent write to `aborted` must be visible at this point
160
+ fence ( Ordering :: Acquire ) ;
161
+
162
+ // The controller was aborted before it was armed; immediately call the abort callback
163
+
164
+ // Fire and forget
165
+ let _ = self
166
+ . js_counterpart
167
+ . get ( )
168
+ . unwrap ( )
169
+ . abort
170
+ . call ( cx, ( self . aborted . get ( ) . unwrap ( ) . clone ( ) , ) ) ;
153
171
}
154
-
155
- // If the Rust controller has already been aborted, call the JS abort callback now
156
- // if let Some(aborted) = self.aborted.get() {
157
- // // Fire and forget
158
- // let _ = js_counterpart.abort.call_on_js_thread((aborted.clone(),));
159
- // }
160
- js_controller. clone ( )
161
- }
162
- Err ( js_controller) => {
163
- // That case is totally unexpected, but should that ever happen, it's ok to just
164
- // return the existing JS controller; the JS objects we just created have not
165
- // yet been exposed anyway, so they will simply get garbage collected.
166
- js_controller. clone ( )
172
+ js_controller
167
173
}
174
+ Err ( js_counterpart) => js_counterpart. controller . to_inner ( cx) . upcast ( ) ,
168
175
} ;
169
176
170
- Ok ( js_controller. to_inner ( cx) )
171
- }
172
-
173
- /// Get a JS `AbortSignal` object for this `AbortController`. Initializes the
174
- /// JS `AbortController` if it hasn't been initialized yet.
175
- fn get_signal < ' cx , C : Context < ' cx > > (
176
- & self ,
177
- cx : & mut C ,
178
- ) -> BridgeResult < Handle < ' cx , JsAbortSignal > > {
179
- let abort_controller = self . get_js_controller ( cx) ?;
180
- Ok ( abort_controller. get ( cx, "signal" ) ?)
177
+ Ok ( controller)
181
178
}
182
179
183
180
/// Immediately abort the `AbortController`, causing the JS side `signal` to fire.
184
- /// The controller is marked as aborted even if the JS counterpart has not yet been initialized;
185
- /// in that case, the abort signal will be fired as soon as the JS side controller is initialized.
186
- ///
187
- /// This method can be called from any thread.
188
181
fn abort ( & self , reason : impl Into < String > ) {
189
182
let reason = reason. into ( ) ;
190
183
if self . aborted . set ( reason. clone ( ) ) == Ok ( ( ) ) {
191
184
// If we haven't created the JS AbortController yet, there's nothing to abort
192
- // VALIDATE: Do we need a memory barrier here to ensure that js_counterpart and aborted are coherent?
193
- if let Some ( js_counterpart) = self . js_counterpart . get ( ) {
194
- // Fire and forget
195
- let _ = js_counterpart. abort . call_on_js_thread ( ( reason, ) ) ;
196
- }
197
- }
198
- }
199
- }
200
-
201
- ////////
185
+ // Ordering: Write to `state` implies previous write to `aborted` is visible to other threads
186
+ if self . state . fetch_or ( STATE_ABORTED , Ordering :: Release ) == STATE_ARMED {
187
+ // Ordering: Previous concurrent write to `js_counterpart` must be visible at this point
188
+ fence ( Ordering :: Acquire ) ;
202
189
203
- struct AbortControllerInnerState2 {
204
- //
205
- js_controller : Option < Root < JsAbortController > > ,
206
- abort_cb : Option < JsCallback < ( String , ) , ( ) > > ,
207
- }
208
-
209
- enum AbortControllerInnerState {
210
- /// The `AbortController` has just been created; the JS counterpart has not yet been initialized.
211
- ///
212
- /// Transitions:
213
- /// - Calling `abort()` will move to the `AbortedBeforeArmed` state
214
- /// - Initializing the JS counterpart will move to the `Armed` state
215
- Uninitialized ,
216
-
217
- /// The `AbortController` has been aborted before the JS counterpart was initialized.
218
- ///
219
- /// Transitions:
220
- /// - Calling `abort()` is a no-op
221
- /// - Initializing the JS counterpart will result in calling the abort
222
- /// callback, and move to the `Disabled` state
223
- AbortedBeforeArmed ( String ) ,
224
-
225
- /// The JS counterpart has been initialized.
226
- ///
227
- /// Transitions:
228
- /// - Calling `abort()` will result in calling the abort callback, and move to the `Disabled` state
229
- /// - Calling `disable()` is a no-op
230
- Armed ( Root < JsAbortController > , JsCallback < ( String , ) , ( ) > ) ,
231
-
232
- /// The `AbortController` has been disarmed, though it was actually never armed.
233
- ///
234
- /// Transitions:
235
- /// - Calling `abort()` is a no-op
236
- /// - Initializing the JS counterpart will move to the `Disarmed` state
237
- UninitializedDisarmed ,
238
-
239
- /// The `AbortController` has been disabled, either by being aborted or by calling `disable()`.
240
- /// It is still possible to obtain a `signal` from the controller, but it's abort status
241
- /// will not change anymore.
242
- ///
243
- /// Any transitions from this state is a no-op.
244
- Disarmed ( Root < JsAbortController > ) ,
245
- }
246
-
247
- impl AbortControllerInnerState {
248
- fn arm (
249
- & mut self ,
250
- js_controller : Root < JsAbortController > ,
251
- abort_cb : JsCallback < ( String , ) , ( ) > ,
252
- ) -> Option < CallAbortDetails > {
253
- match self {
254
- Self :: Uninitialized => {
255
- * self = Self :: Armed ( js_controller, abort_cb) ;
256
- None
257
- }
258
- Self :: AbortedBeforeArmed ( abort_reason) => {
259
- let abort_reason = abort_reason. clone ( ) ;
260
- * self = Self :: Disarmed ( js_controller) ;
261
- Some ( CallAbortDetails {
262
- abort_reason,
263
- abort_cb,
264
- } )
265
- }
266
- Self :: UninitializedDisarmed => {
267
- * self = Self :: Disarmed ( js_controller) ;
268
- None
269
- }
270
- _ => None ,
271
- }
272
- }
273
-
274
- fn abort ( & mut self , reason : impl Into < String > ) -> Option < CallAbortDetails > {
275
- let current = mem:: replace ( self , Self :: Uninitialized ) ;
276
- match current {
277
- Self :: Uninitialized => {
278
- * self = Self :: AbortedBeforeArmed ( reason. into ( ) ) ;
279
- None
280
- }
281
- Self :: Armed ( js_controller, abort_cb) => {
282
- * self = Self :: Disarmed ( js_controller) ;
283
- Some ( CallAbortDetails {
284
- abort_reason : reason. into ( ) ,
285
- abort_cb,
286
- } )
287
- }
288
- other => {
289
- * self = other;
290
- None
190
+ // Fire and forget
191
+ let _ = self
192
+ . js_counterpart
193
+ . get ( )
194
+ . unwrap ( )
195
+ . abort
196
+ . call_on_js_thread ( ( reason, ) ) ;
291
197
}
292
198
}
293
199
}
294
200
295
- fn disarm ( & mut self ) {
296
- let current = mem:: replace ( self , Self :: Uninitialized ) ;
297
- match current {
298
- Self :: Uninitialized => {
299
- * self = Self :: UninitializedDisarmed ;
300
- }
301
- Self :: Armed ( js_controller, _) => {
302
- * self = Self :: Disarmed ( js_controller) ;
303
- }
304
- other => {
305
- * self = other;
306
- }
307
- }
201
+ fn disarm ( & self ) {
202
+ // Ordering: this requires no dependency on any other state
203
+ self . state . store ( STATE_DISARMED , Ordering :: Relaxed ) ;
308
204
}
309
205
}
310
206
311
- struct CallAbortDetails {
312
- abort_reason : String ,
313
- abort_cb : JsCallback < ( String , ) , ( ) > ,
207
+ # [ cfg ( feature = "test" ) ]
208
+ mod tests {
209
+ use super :: * ;
314
210
}
0 commit comments