Skip to content

Commit e4d2697

Browse files
authored
Merge pull request #17 from github/abort-controller
Abort in-flight requests
2 parents dff602d + a466233 commit e4d2697

File tree

3 files changed

+145
-94
lines changed

3 files changed

+145
-94
lines changed

examples/index.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@
1111
function fakeFetch(url) {
1212
const urlObj = new URL(url)
1313
let html = ''
14-
if (url.pathname === '/results') {
14+
if (urlObj.pathname === '/results') {
1515
const doc = document.createElement('div')
1616
doc.innerHTML = `<li>Hubot</li><li>BB-8</li><li>Wall-E</li><li>Bender</li>`
17-
const q = url.searchParams.get('q')
17+
const q = urlObj.searchParams.get('q')
1818
for (const el of doc.querySelectorAll('li')) {
1919
if (q !== '' && !el.textContent.toLowerCase().match(q.toLowerCase())) el.remove()
2020
}
2121
html = doc.innerHTML
22-
} else if (url.pathname === '/marquee') {
23-
html = `<marquee>${url.searchParams.get('q') || '🐈 Nothing to preview 🐈'}</marquee>`
22+
} else if (urlObj.pathname === '/marquee') {
23+
html = `<marquee>${urlObj.searchParams.get('q') || '🐈 Nothing to preview 🐈'}</marquee>`
2424
}
25-
const promiseHTML = new Promise(resolve => resolve(html))
26-
return new Promise(resolve => resolve({ok: true, text: () => promiseHTML}))
25+
return Promise.resolve({ok: true, text: () => Promise.resolve(html)})
2726
}
2827
window.fetch = fakeFetch
2928
</script>
@@ -40,9 +39,9 @@
4039
<ul id="results"></ul>
4140

4241
<!-- GitHub Pages development script, uncomment when running example locally and comment out the production one -->
43-
<!-- <script src="../dist/index.umd.js"></script> -->
42+
<!-- <script type="module" src="../dist/index.js"></script> -->
4443

4544
<!-- GitHub Pages demo script -->
46-
<script src="https://unpkg.com/@github/remote-input-element@latest/dist/index.umd.js"></script>
45+
<script type="module" src="https://unpkg.com/@github/remote-input-element@latest/dist/index.js"></script>
4746
</body>
4847
</html>

src/index.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class RemoteInputElement extends HTMLElement {
44
constructor() {
55
super()
66
const fetch = fetchResults.bind(null, this, true)
7-
const state = {currentQuery: null, oninput: debounce(fetch), fetch}
7+
const state = {currentQuery: null, oninput: debounce(fetch), fetch, controller: null}
88
states.set(this, state)
99
}
1010

@@ -59,6 +59,13 @@ class RemoteInputElement extends HTMLElement {
5959
}
6060
}
6161

62+
function makeAbortController() {
63+
if ('AbortController' in window) {
64+
return new AbortController()
65+
}
66+
return {signal: null, abort() {}}
67+
}
68+
6269
async function fetchResults(remoteInput: RemoteInputElement, checkCurrentQuery: boolean) {
6370
const input = remoteInput.input
6471
if (!input) return
@@ -82,33 +89,55 @@ async function fetchResults(remoteInput: RemoteInputElement, checkCurrentQuery:
8289
params.append(remoteInput.getAttribute('param') || 'q', query)
8390
url.search = params.toString()
8491

85-
remoteInput.dispatchEvent(new CustomEvent('loadstart'))
86-
remoteInput.setAttribute('loading', '')
92+
if (state.controller) {
93+
state.controller.abort()
94+
} else {
95+
remoteInput.dispatchEvent(new CustomEvent('loadstart'))
96+
remoteInput.setAttribute('loading', '')
97+
}
98+
99+
state.controller = makeAbortController()
100+
87101
let response
88-
let errored = false
89102
let html = ''
90103
try {
91-
response = await fetch(url.toString(), {
104+
response = await fetchWithNetworkEvents(remoteInput, url.toString(), {
105+
signal: state.controller.signal,
92106
credentials: 'same-origin',
93107
headers: {accept: 'text/html; fragment'}
94108
})
95109
html = await response.text()
96-
remoteInput.dispatchEvent(new CustomEvent('load'))
97-
} catch {
98-
errored = true
99-
remoteInput.dispatchEvent(new CustomEvent('error'))
110+
remoteInput.removeAttribute('loading')
111+
state.controller = null
112+
} catch (error) {
113+
if (error.name !== 'AbortError') {
114+
remoteInput.removeAttribute('loading')
115+
state.controller = null
116+
}
117+
return
100118
}
101-
remoteInput.removeAttribute('loading')
102-
if (errored) return
103119

104120
if (response && response.ok) {
105-
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
106121
resultsContainer.innerHTML = html
122+
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
107123
} else {
108124
remoteInput.dispatchEvent(new CustomEvent('remote-input-error', {bubbles: true}))
109125
}
126+
}
110127

111-
remoteInput.dispatchEvent(new CustomEvent('loadend'))
128+
async function fetchWithNetworkEvents(el: Element, url: string, options: RequestInit): Promise<Response> {
129+
try {
130+
const response = await fetch(url, options)
131+
el.dispatchEvent(new CustomEvent('load'))
132+
el.dispatchEvent(new CustomEvent('loadend'))
133+
return response
134+
} catch (error) {
135+
if (error.name !== 'AbortError') {
136+
el.dispatchEvent(new CustomEvent('error'))
137+
el.dispatchEvent(new CustomEvent('loadend'))
138+
}
139+
throw error
140+
}
112141
}
113142

114143
function debounce(callback: () => void) {

test/test.js

Lines changed: 96 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,107 +12,130 @@ describe('remote-input', function() {
1212
})
1313

1414
describe('after tree insertion', function() {
15+
let remoteInput
16+
let input
17+
let results
18+
1519
beforeEach(function() {
1620
document.body.innerHTML = `
1721
<remote-input aria-owns="results" src="/results">
1822
<input>
1923
</remote-input>
2024
<div id="results"></div>
2125
`
26+
remoteInput = document.querySelector('remote-input')
27+
input = remoteInput.querySelector('input')
28+
results = document.querySelector('#results')
2229
})
2330

2431
afterEach(function() {
2532
document.body.innerHTML = ''
33+
remoteInput = null
34+
input = null
35+
results = null
2636
})
2737

28-
it('loads content', function(done) {
29-
const remoteInput = document.querySelector('remote-input')
30-
const input = document.querySelector('input')
31-
const results = document.querySelector('#results')
38+
it('emits network events in order', async function() {
39+
const events = []
40+
const track = event => events.push(event.type)
41+
42+
remoteInput.addEventListener('loadstart', track)
43+
remoteInput.addEventListener('load', track)
44+
remoteInput.addEventListener('loadend', track)
45+
46+
const completed = Promise.all([
47+
once(remoteInput, 'loadstart'),
48+
once(remoteInput, 'load'),
49+
once(remoteInput, 'loadend')
50+
])
51+
changeValue(input, 'test')
52+
await completed
53+
54+
assert.deepEqual(['loadstart', 'load', 'loadend'], events)
55+
})
56+
57+
it('loads content', async function() {
3258
assert.equal(results.innerHTML, '')
33-
let successEvent = false
34-
remoteInput.addEventListener('remote-input-success', function() {
35-
successEvent = true
36-
})
37-
remoteInput.addEventListener('loadend', function() {
38-
assert.ok(successEvent, 'success event happened')
39-
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
40-
done()
41-
})
42-
input.value = 'test'
43-
input.focus()
59+
60+
const success = once(remoteInput, 'remote-input-success')
61+
const loadend = once(remoteInput, 'loadend')
62+
63+
changeValue(input, 'test')
64+
65+
await success
66+
await loadend
67+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
4468
})
4569

46-
it('handles not ok responses', function(done) {
47-
const remoteInput = document.querySelector('remote-input')
48-
const input = document.querySelector('input')
49-
const results = document.querySelector('#results')
70+
it('handles not ok responses', async function() {
5071
remoteInput.src = '/500'
5172
assert.equal(results.innerHTML, '')
52-
let errorEvent = false
53-
remoteInput.addEventListener('remote-input-error', function() {
54-
errorEvent = true
55-
})
56-
remoteInput.addEventListener('loadend', function() {
57-
assert.ok(errorEvent, 'error event happened')
58-
assert.equal(results.innerHTML, '', 'nothing was appended')
59-
done()
60-
})
61-
input.value = 'test'
62-
input.focus()
73+
74+
const error = once(remoteInput, 'remote-input-error')
75+
const loadend = once(remoteInput, 'loadend')
76+
77+
changeValue(input, 'test')
78+
79+
await loadend
80+
await error
81+
82+
assert.equal(results.innerHTML, '', 'nothing was appended')
6383
})
6484

65-
it('handles network error', function(done) {
66-
const remoteInput = document.querySelector('remote-input')
67-
const input = document.querySelector('input')
68-
const results = document.querySelector('#results')
85+
it('handles network error', async function() {
6986
remoteInput.src = '/network-error'
7087
assert.equal(results.innerHTML, '')
71-
remoteInput.addEventListener('error', async function() {
72-
await Promise.resolve()
73-
assert.equal(results.innerHTML, '', 'nothing was appended')
74-
assert.notOk(remoteInput.hasAttribute('loading'), 'loading attribute was removed')
75-
done()
76-
})
77-
input.value = 'test'
78-
input.focus()
79-
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute was added')
88+
89+
const result = once(remoteInput, 'error')
90+
91+
changeValue(input, 'test')
92+
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute should have been added')
93+
94+
await result
95+
await nextTick()
96+
assert.equal(results.innerHTML, '', 'nothing was appended')
97+
assert.notOk(remoteInput.hasAttribute('loading'), 'loading attribute should have been removed')
8098
})
8199

82-
it('repects param attribute', function(done) {
83-
const remoteInput = document.querySelector('remote-input')
84-
const input = document.querySelector('input')
85-
const results = document.querySelector('#results')
100+
it('repects param attribute', async function() {
86101
remoteInput.setAttribute('param', 'robot')
87102
assert.equal(results.innerHTML, '')
88-
remoteInput.addEventListener('loadend', function() {
89-
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
90-
done()
91-
})
92-
input.value = 'test'
93-
input.focus()
103+
104+
const result = once(remoteInput, 'remote-input-success')
105+
106+
changeValue(input, 'test')
107+
108+
await result
109+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
94110
})
95111

96-
it('loads content again after src is changed', function(done) {
97-
const remoteInput = document.querySelector('remote-input')
98-
const input = document.querySelector('input')
99-
const results = document.querySelector('#results')
100-
101-
function listenOnce(cb) {
102-
remoteInput.addEventListener('loadend', cb, {once: true})
103-
}
104-
listenOnce(function() {
105-
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
106-
107-
listenOnce(function() {
108-
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/srcChanged?q=test')
109-
done()
110-
})
111-
112-
remoteInput.src = '/srcChanged'
113-
})
114-
input.value = 'test'
115-
input.focus()
112+
it('loads content again after src is changed', async function() {
113+
const result1 = once(remoteInput, 'remote-input-success')
114+
changeValue(input, 'test')
115+
116+
await result1
117+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
118+
119+
const result2 = once(remoteInput, 'remote-input-success')
120+
remoteInput.src = '/srcChanged'
121+
122+
await result2
123+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/srcChanged?q=test')
116124
})
117125
})
118126
})
127+
128+
function changeValue(input, value) {
129+
input.value = value
130+
input.dispatchEvent(new Event('change'))
131+
}
132+
133+
function nextTick() {
134+
return Promise.resolve()
135+
}
136+
137+
function once(element, eventName) {
138+
return new Promise(resolve => {
139+
element.addEventListener(eventName, resolve, {once: true})
140+
})
141+
}

0 commit comments

Comments
 (0)