Skip to content

Commit 7026d33

Browse files
committed
feat: add polyfill that only supports AbortSignal
1 parent 0811baf commit 7026d33

File tree

6 files changed

+244
-42
lines changed

6 files changed

+244
-42
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ IE 11 you also need to polyfill promises. One caveat is that CORS requests will
146146

147147
Here is a basic example of [abortable fetch running in IE 8](https://github.com/mo/abortcontroller-polyfill-examples/tree/master/plain-javascript-fetch-ie8).
148148

149+
# Using it on Legacy Webview
150+
151+
The ```abortcontroller-polyfill``` works on Legacy Webview. However, to use it you must first confirm that the native AbortController and AbortSignal already exist in the environment. Otherwise, it is recommended to use the other solutions mentioned above.
152+
153+
```js
154+
import 'abortcontroller-polyfill/dist/abortsignal-polyfill-only';
155+
```
156+
149157
# Contributors
150158
* [Martin Olsson](https://github.com/mo)
151159
* [Jimmy Wärting](https://github.com/jimmywarting)

rollup.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export default [{
3030
},
3131
plugins
3232
},
33+
{
34+
input: 'src/abortsignal-polyfill.js',
35+
output: {
36+
file: 'dist/abortsignal-polyfill-only.js',
37+
format: 'umd'
38+
},
39+
plugins
40+
},
3341
{
3442
input: 'src/ponyfill.js',
3543
output: {
@@ -46,4 +54,13 @@ export default [{
4654
exports: 'named'
4755
},
4856
plugins
57+
},
58+
{
59+
input: 'src/abortsignal-ponyfill.js',
60+
output: {
61+
file: 'dist/abortsignal-ponyfill.js',
62+
format: 'cjs',
63+
exports: 'named'
64+
},
65+
plugins
4966
}];

src/abortcontroller.js

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createAbortEvent, normalizeAbortReason } from "./abortsignal-ponyfill";
2+
13
class Emitter {
24
constructor() {
35
Object.defineProperty(this, 'listeners', { value: {}, writable: true, configurable: true });
@@ -139,48 +141,8 @@ export class AbortController {
139141
Object.defineProperty(this, 'signal', { value: new AbortSignal(), writable: true, configurable: true });
140142
}
141143
abort(reason) {
142-
let event;
143-
try {
144-
event = new Event('abort');
145-
} catch (e) {
146-
if (typeof document !== 'undefined') {
147-
if (!document.createEvent) {
148-
// For Internet Explorer 8:
149-
event = document.createEventObject();
150-
event.type = 'abort';
151-
} else {
152-
// For Internet Explorer 11:
153-
event = document.createEvent('Event');
154-
event.initEvent('abort', false, false);
155-
}
156-
} else {
157-
// Fallback where document isn't available:
158-
event = {
159-
type: 'abort',
160-
bubbles: false,
161-
cancelable: false,
162-
};
163-
}
164-
}
165-
166-
let signalReason = reason;
167-
if (signalReason === undefined) {
168-
if (typeof document === 'undefined') {
169-
signalReason = new Error('This operation was aborted');
170-
signalReason.name = 'AbortError';
171-
} else {
172-
try {
173-
signalReason = new DOMException('signal is aborted without reason');
174-
signalReason.name = 'AbortError';
175-
} catch (err) {
176-
// IE 11 does not support calling the DOMException constructor, use a
177-
// regular error object on it instead.
178-
signalReason = new Error('This operation was aborted');
179-
signalReason.name = 'AbortError';
180-
}
181-
}
182-
}
183-
this.signal.reason = signalReason;
144+
const signalReason = normalizeAbortReason(reason)
145+
const event = createAbortEvent(signalReason)
184146

185147
this.signal.dispatchEvent(event);
186148
}

src/abortsignal-polyfill.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { abortsignalPonyfill, normalizeAbortReason, createAbortEvent } from "./abortsignal-ponyfill";
2+
import { signalPolyfillNeeded } from "./utils";
3+
4+
(function (self) {
5+
'use strict';
6+
7+
if (!signalPolyfillNeeded(self)) {
8+
return;
9+
}
10+
11+
self.AbortSignal.abort = function abort(reason) {
12+
const ac = new AbortController()
13+
ac.abort(reason)
14+
return ac.signal
15+
}
16+
17+
self.AbortSignal.any = function any(iterable) {
18+
const controller = new AbortController();
19+
/**
20+
* @this AbortSignal
21+
*/
22+
function abort() {
23+
controller.abort(this.reason);
24+
clean();
25+
}
26+
function clean() {
27+
for (const signal of iterable) signal.removeEventListener('abort', abort);
28+
}
29+
for (const signal of iterable)
30+
if (signal.aborted) {
31+
controller.abort(signal.reason);
32+
break;
33+
} else signal.addEventListener('abort', abort);
34+
35+
return controller.signal;
36+
}
37+
38+
self.AbortSignal.timeout = function timeout(ms) {
39+
const controller = new AbortController();
40+
41+
setTimeout(() => controller.abort(new DOMException(`This signal is timeout in ${time}ms`, 'TimeoutError')), time);
42+
43+
return controller.signal;
44+
}
45+
46+
const NativeAbortController = self.AbortController
47+
if (!NativeAbortController.__polyfill__) {
48+
self.AbortController = class AbortController extends NativeAbortController {
49+
constructor() {
50+
super()
51+
abortsignalPonyfill(this.signal)
52+
}
53+
54+
static __polyfill__ = true
55+
56+
abort(reason) {
57+
if (!this.signal.aborted) {
58+
super.abort(reason)
59+
60+
if (this.signal.__ponyfill__) {
61+
const signalReason = normalizeAbortReason(reason)
62+
const event = createAbortEvent(signalReason)
63+
64+
this.signal._reason = signalReason
65+
this.signal.dispatchEvent(event)
66+
}
67+
}
68+
}
69+
}
70+
}
71+
})(typeof self !== 'undefined' ? self : global);

src/abortsignal-ponyfill.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Make the native AbortSignal instances support the reason property and the throwIfAborted method.
3+
* @param {AbortSignal} signal native AbortSignal instance
4+
* @returns {AbortSignal} AbortSignal instance
5+
*/
6+
export function abortsignalPonyfill(signal) {
7+
if (!('reason' in signal)) {
8+
signal._reason = undefined
9+
signal._onabort = null
10+
11+
Object.defineProperties(signal, {
12+
__ponyfill__: {
13+
value: true,
14+
},
15+
16+
reason: {
17+
get() {
18+
return this._reason
19+
}
20+
},
21+
22+
onabort: {
23+
get() {
24+
return this._onabort
25+
},
26+
set(callback) {
27+
const existing = this._onabort
28+
if (existing) {
29+
this.removeEventListener('abort', existing)
30+
}
31+
this._onabort = callback
32+
this.addEventListener('abort', callback)
33+
}
34+
}
35+
})
36+
37+
const { dispatchEvent, addEventListener, removeEventListener } = signal
38+
39+
signal.addEventListener = function (type, callback, options) {
40+
if (type === 'abort' && callback && this.__ponyfill__) {
41+
if (!callback.__ponyfill__) {
42+
const rawCallback = callback
43+
Object.defineProperty(callback, '__ponyfill__', {
44+
value(e) {
45+
if (e.__ponyfill__) {
46+
return rawCallback.call(this, e)
47+
}
48+
}
49+
})
50+
}
51+
callback = callback.__ponyfill__
52+
}
53+
return addEventListener.call(this, type, callback, options)
54+
}
55+
56+
signal.removeEventListener = function (type, callback, options) {
57+
if (type === 'abort' && callback && this.__ponyfill__ && callback.__ponyfill__) {
58+
callback = callback.__ponyfill__
59+
}
60+
return removeEventListener.call(this, type, callback, options)
61+
}
62+
63+
signal.dispatchEvent = function (event) {
64+
if (event.type === 'abort') {
65+
Object.defineProperty(event, '__ponyfill__', {
66+
value: true,
67+
})
68+
}
69+
return dispatchEvent.call(this, event)
70+
}
71+
}
72+
73+
if (!('throwIfAborted') in signal) {
74+
signal.throwIfAborted = function throwIfAborted() {
75+
if (this.aborted) {
76+
throw this.reason
77+
}
78+
}
79+
}
80+
81+
return signal
82+
}
83+
84+
/**
85+
* @param {any} reason abort reason
86+
*/
87+
export function createAbortEvent(reason) {
88+
let event;
89+
try {
90+
event = new Event('abort');
91+
} catch (e) {
92+
if (typeof document !== 'undefined') {
93+
if (!document.createEvent) {
94+
// For Internet Explorer 8:
95+
event = document.createEventObject();
96+
event.type = 'abort';
97+
} else {
98+
// For Internet Explorer 11:
99+
event = document.createEvent('Event');
100+
event.initEvent('abort', false, false);
101+
}
102+
} else {
103+
// Fallback where document isn't available:
104+
event = {
105+
type: 'abort',
106+
bubbles: false,
107+
cancelable: false,
108+
};
109+
}
110+
}
111+
event.reason = reason
112+
return event
113+
}
114+
115+
/**
116+
* @param {any} reason abort reason
117+
*/
118+
export function normalizeAbortReason(reason) {
119+
if (reason === undefined) {
120+
if (typeof document === 'undefined') {
121+
reason = new Error('This operation was aborted');
122+
reason.name = 'AbortError';
123+
} else {
124+
try {
125+
reason = new DOMException('signal is aborted without reason');
126+
reason.name = 'AbortError';
127+
} catch (err) {
128+
// IE 11 does not support calling the DOMException constructor, use a
129+
// regular error object on it instead.
130+
reason = new Error('This operation was aborted');
131+
reason.name = 'AbortError';
132+
}
133+
}
134+
}
135+
return reason
136+
}

src/utils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ export function polyfillNeeded(self) {
1515
(typeof self.Request === 'function' && !self.Request.prototype.hasOwnProperty('signal')) || !self.AbortController
1616
);
1717
}
18+
19+
export function signalPolyfillNeeded(self) {
20+
return (
21+
!!self.AbortController &&
22+
typeof self.AbortSignal === 'function' &&
23+
!self.AbortSignal.prototype.hasOwnProperty('reason')
24+
);
25+
}

0 commit comments

Comments
 (0)