Skip to content

Commit 6698b4b

Browse files
dgrahammuan
andcommitted
Abort in-flight requests
Announce a single set of logical network events—loadstart, load, loadend—while aborting all but the final request as an internal optimization. Closes #14 Co-authored-by: Mu-An Chiou <me@muanchiou.com>
1 parent dff602d commit 6698b4b

File tree

2 files changed

+97
-60
lines changed

2 files changed

+97
-60
lines changed

src/index.ts

Lines changed: 39 additions & 12 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,53 @@ 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'))
92+
if (state.controller) {
93+
state.controller.abort()
94+
} else {
95+
remoteInput.dispatchEvent(new CustomEvent('loadstart'))
96+
}
97+
98+
state.controller = makeAbortController()
99+
86100
remoteInput.setAttribute('loading', '')
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+
} catch (error) {
112+
if (error.name !== 'AbortError') {
113+
remoteInput.removeAttribute('loading')
114+
}
115+
return
100116
}
101-
remoteInput.removeAttribute('loading')
102-
if (errored) return
103117

104118
if (response && response.ok) {
105-
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
106119
resultsContainer.innerHTML = html
120+
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
107121
} else {
108122
remoteInput.dispatchEvent(new CustomEvent('remote-input-error', {bubbles: true}))
109123
}
124+
}
110125

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

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

test/test.js

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,94 +25,104 @@ describe('remote-input', function() {
2525
document.body.innerHTML = ''
2626
})
2727

28-
it('loads content', function(done) {
28+
it('loads content', async function() {
2929
const remoteInput = document.querySelector('remote-input')
3030
const input = document.querySelector('input')
3131
const results = document.querySelector('#results')
3232
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-
})
33+
34+
const success = once(remoteInput, 'remote-input-success')
35+
const loadend = once(remoteInput, 'loadend')
36+
4237
input.value = 'test'
4338
input.focus()
39+
40+
await success
41+
await loadend
42+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
4443
})
4544

46-
it('handles not ok responses', function(done) {
45+
it('handles not ok responses', async function() {
4746
const remoteInput = document.querySelector('remote-input')
4847
const input = document.querySelector('input')
4948
const results = document.querySelector('#results')
5049
remoteInput.src = '/500'
5150
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-
})
51+
52+
const error = once(remoteInput, 'remote-input-error')
53+
const loadend = once(remoteInput, 'loadend')
54+
6155
input.value = 'test'
6256
input.focus()
57+
58+
await loadend
59+
await error
60+
61+
assert.equal(results.innerHTML, '', 'nothing was appended')
6362
})
6463

65-
it('handles network error', function(done) {
64+
it('handles network error', async function() {
6665
const remoteInput = document.querySelector('remote-input')
67-
const input = document.querySelector('input')
66+
const input = remoteInput.querySelector('input')
6867
const results = document.querySelector('#results')
6968
remoteInput.src = '/network-error'
7069
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-
})
70+
71+
const result = once(remoteInput, 'error')
72+
7773
input.value = 'test'
7874
input.focus()
79-
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute was added')
75+
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute should have been added')
76+
77+
await result
78+
await nextTick()
79+
assert.equal(results.innerHTML, '', 'nothing was appended')
80+
assert.notOk(remoteInput.hasAttribute('loading'), 'loading attribute should have been removed')
8081
})
8182

82-
it('repects param attribute', function(done) {
83+
it('repects param attribute', async function() {
8384
const remoteInput = document.querySelector('remote-input')
8485
const input = document.querySelector('input')
8586
const results = document.querySelector('#results')
8687
remoteInput.setAttribute('param', 'robot')
8788
assert.equal(results.innerHTML, '')
88-
remoteInput.addEventListener('loadend', function() {
89-
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
90-
done()
91-
})
89+
90+
const result = once(remoteInput, 'remote-input-success')
91+
9292
input.value = 'test'
9393
input.focus()
94+
95+
await result
96+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
9497
})
9598

96-
it('loads content again after src is changed', function(done) {
99+
it('loads content again after src is changed', async function() {
97100
const remoteInput = document.querySelector('remote-input')
98101
const input = document.querySelector('input')
99102
const results = document.querySelector('#results')
100103

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-
})
104+
const result1 = once(remoteInput, 'remote-input-success')
114105
input.value = 'test'
115106
input.focus()
107+
108+
await result1
109+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
110+
111+
const result2 = once(remoteInput, 'remote-input-success')
112+
remoteInput.src = '/srcChanged'
113+
114+
await result2
115+
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/srcChanged?q=test')
116116
})
117117
})
118118
})
119+
120+
function nextTick() {
121+
return Promise.resolve()
122+
}
123+
124+
function once(element, eventName) {
125+
return new Promise(resolve => {
126+
element.addEventListener(eventName, resolve, {once: true})
127+
})
128+
}

0 commit comments

Comments
 (0)