Skip to content

Commit 438b9cf

Browse files
jasnelldanielleadams
authored andcommitted
lib: make AbortSignal cloneable/transferable
Allows for using `AbortSignal` across worker threads and contexts. ```js const ac = new AbortController(); const mc = new MessageChannel(); mc.port1.onmessage = ({ data }) => { data.addEventListener('abort', () => { console.log('aborted!'); }); }; mc.port2.postMessage(ac.signal, [ac.signal]); ``` Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #41050 Refs: whatwg/dom#948 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent 799cf57 commit 438b9cf

2 files changed

Lines changed: 169 additions & 5 deletions

File tree

‎lib/internal/abort_controller.js‎

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,37 @@ const {
4747
setTimeout,
4848
}=require('timers');
4949

50-
constkAborted=Symbol('kAborted');
51-
constkReason=Symbol('kReason');
52-
constkTimeout=Symbol('kTimeout');
50+
const{
51+
messaging_deserialize_symbol: kDeserialize,
52+
messaging_transfer_symbol: kTransfer,
53+
messaging_transfer_list_symbol: kTransferList
54+
}=internalBinding('symbols');
5355

54-
consttimeOutSignals=newSafeSet();
56+
let_MessageChannel;
57+
letmakeTransferable;
58+
59+
// Loading the MessageChannel and makeTransferable have to be done lazily
60+
// because otherwise we'll end up with a require cycle that ends up with
61+
// an incomplete initialization of abort_controller.
62+
63+
functionlazyMessageChannel(){
64+
_MessageChannel??=require('internal/worker/io').MessageChannel;
65+
returnnew_MessageChannel();
66+
}
67+
68+
functionlazyMakeTransferable(obj){
69+
makeTransferable??=
70+
require('internal/worker/js_transferable').makeTransferable;
71+
returnmakeTransferable(obj);
72+
}
5573

5674
constclearTimeoutRegistry=newSafeFinalizationRegistry(clearTimeout);
75+
consttimeOutSignals=newSafeSet();
76+
77+
constkAborted=Symbol('kAborted');
78+
constkReason=Symbol('kReason');
79+
constkCloneData=Symbol('kCloneData');
80+
constkTimeout=Symbol('kTimeout');
5781

5882
functioncustomInspect(self,obj,depth,options){
5983
if(depth<0)
@@ -165,7 +189,68 @@ class AbortSignal extends EventTarget {
165189
timeOutSignals.delete(this);
166190
}
167191
}
192+
193+
[kTransfer](){
194+
validateAbortSignal(this);
195+
constaborted=this.aborted;
196+
if(aborted){
197+
constreason=this.reason;
198+
return{
199+
data: { aborted, reason },
200+
deserializeInfo: 'internal/abort_controller:ClonedAbortSignal',
201+
};
202+
}
203+
204+
const{ port1, port2 }=this[kCloneData];
205+
this[kCloneData]=undefined;
206+
207+
this.addEventListener('abort',()=>{
208+
port1.postMessage(this.reason);
209+
port1.close();
210+
},{once: true});
211+
212+
return{
213+
data: {port: port2},
214+
deserializeInfo: 'internal/abort_controller:ClonedAbortSignal',
215+
};
216+
}
217+
218+
[kTransferList](){
219+
if(!this.aborted){
220+
const{ port1, port2 }=lazyMessageChannel();
221+
port1.unref();
222+
port2.unref();
223+
this[kCloneData]={
224+
port1,
225+
port2,
226+
};
227+
return[port2];
228+
}
229+
return[];
230+
}
231+
232+
[kDeserialize]({ aborted, reason, port }){
233+
if(aborted){
234+
this[kAborted]=aborted;
235+
this[kReason]=reason;
236+
return;
237+
}
238+
239+
port.onmessage=({ data })=>{
240+
abortSignal(this,data);
241+
port.close();
242+
port.onmessage=undefined;
243+
};
244+
// The receiving port, by itself, should never keep the event loop open.
245+
// The unref() has to be called *after* setting the onmessage handler.
246+
port.unref();
247+
}
248+
}
249+
250+
functionClonedAbortSignal(){
251+
returncreateAbortSignal();
168252
}
253+
ClonedAbortSignal.prototype[kDeserialize]=()=>{};
169254

170255
ObjectDefineProperties(AbortSignal.prototype,{
171256
aborted: {enumerable: true}
@@ -185,7 +270,7 @@ function createAbortSignal(aborted = false, reason = undefined) {
185270
ObjectSetPrototypeOf(signal,AbortSignal.prototype);
186271
signal[kAborted]=aborted;
187272
signal[kReason]=reason;
188-
returnsignal;
273+
returnlazyMakeTransferable(signal);
189274
}
190275

191276
functionabortSignal(signal,reason){
@@ -252,4 +337,5 @@ module.exports = {
252337
kAborted,
253338
AbortController,
254339
AbortSignal,
340+
ClonedAbortSignal,
255341
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
const{ ok, strictEqual }=require('assert');
5+
const{setImmediate: pause}=require('timers/promises');
6+
7+
functiondeferred(){
8+
letres;
9+
constpromise=newPromise((resolve)=>res=resolve);
10+
return{ res, promise };
11+
}
12+
13+
(async()=>{
14+
constac=newAbortController();
15+
constmc=newMessageChannel();
16+
17+
constdeferred1=deferred();
18+
constdeferred2=deferred();
19+
constresolvers=[deferred1,deferred2];
20+
21+
mc.port1.onmessage=common.mustCall(({ data })=>{
22+
data.addEventListener('abort',common.mustCall(()=>{
23+
strictEqual(data.reason,'boom');
24+
}));
25+
resolvers.shift().res();
26+
},2);
27+
28+
mc.port2.postMessage(ac.signal,[ac.signal]);
29+
30+
// Can be cloned/transferd multiple times and they all still work
31+
mc.port2.postMessage(ac.signal,[ac.signal]);
32+
33+
mc.port2.close();
34+
35+
// Although we're using transfer semantics, the local AbortSignal
36+
// is still usable locally.
37+
ac.signal.addEventListener('abort',common.mustCall(()=>{
38+
strictEqual(ac.signal.reason,'boom');
39+
}));
40+
41+
awaitPromise.all([deferred1.promise,deferred2.promise]);
42+
43+
ac.abort('boom');
44+
45+
// Because the postMessage used by the underlying AbortSignal
46+
// takes at least one turn of the event loop to be processed,
47+
// and because it is unref'd, it won't, by itself, keep the
48+
// event loop open long enough for the test to complete, so
49+
// we schedule two back to back turns of the event to ensure
50+
// the loop runs long enough for the test to complete.
51+
awaitpause();
52+
awaitpause();
53+
54+
})().then(common.mustCall());
55+
56+
{
57+
constsignal=AbortSignal.abort('boom');
58+
ok(signal.aborted);
59+
strictEqual(signal.reason,'boom');
60+
constmc=newMessageChannel();
61+
mc.port1.onmessage=common.mustCall(({ data })=>{
62+
ok(datainstanceofAbortSignal);
63+
ok(data.aborted);
64+
strictEqual(data.reason,'boom');
65+
mc.port1.close();
66+
});
67+
mc.port2.postMessage(signal,[signal]);
68+
}
69+
70+
{
71+
// The cloned AbortSignal does not keep the event loop open
72+
// waiting for the abort to be triggered.
73+
constac=newAbortController();
74+
constmc=newMessageChannel();
75+
mc.port1.onmessage=common.mustCall();
76+
mc.port2.postMessage(ac.signal,[ac.signal]);
77+
mc.port2.close();
78+
}

0 commit comments

Comments
 (0)