Skip to content

Commit

Permalink
Better AbortController polyfill
Browse files Browse the repository at this point in the history
Still minimal, but plays nicer with things that expect to listen for
'abort' events using addEventListener.

Fix: #230
  • Loading branch information
isaacs committed Apr 30, 2022
1 parent 955935a commit 2cbea7e
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 14 deletions.
12 changes: 9 additions & 3 deletions README.md
Expand Up @@ -140,9 +140,15 @@ Function that is used to make background asynchronous fetches. Called with
If `fetchMethod` is not provided, then `cache.fetch(key)` is equivalent to
`Promise.resolve(cache.get(key))`.

The `signal` object is an `AbortSignal`. If at any time, `signal.aborted` is
set to `true`, then that means that the fetch should be abandoned. This may be
passed along to async functions aware of AbortController/AbortSignal behavior.
The `signal` object is an `AbortSignal` if that's available in
the global object, otherwise it's a pretty close polyfill.

If at any time, `signal.aborted` is set to `true`, or if the
`signal.onabort` method is called, or if it emits an `'abort'`
event which you can listen to with `addEventListener`, then that
means that the fetch should be abandoned. This may be passed
along to async functions aware of AbortController/AbortSignal
behavior.

The `options` object is a union of the options that may be provided to `set()`
and `get()`. If they are modified, then that will result in modifying the
Expand Down
43 changes: 40 additions & 3 deletions index.js
@@ -1,15 +1,46 @@
const perf = typeof performance === 'object' && performance &&
typeof performance.now === 'function' ? performance : Date

const hasAbortController = typeof AbortController !== 'undefined'
const hasAbortController = typeof AbortController === 'function'

// minimal backwards-compatibility polyfill
// this doesn't have nearly all the checks and whatnot that
// actual AbortController/Signal has, but it's enough for
// our purposes, and if used properly, behaves the same.
const AC = hasAbortController ? AbortController : Object.assign(
class AbortController {
constructor () { this.signal = new AC.AbortSignal }
abort () { this.signal.aborted = true }
abort () {
this.signal.dispatchEvent('abort')
}
},
{ AbortSignal: class AbortSignal { constructor () { this.aborted = false }}}
{
AbortSignal: class AbortSignal {
constructor () {
this.aborted = false
this._listeners = []
}
dispatchEvent (type) {
if (type === 'abort') {
this.aborted = true
const e = { type, target: this }
this.onabort(e)
this._listeners.forEach(f => f(e), this)
}
}
onabort () {}
addEventListener (ev, fn) {
if (ev === 'abort') {
this._listeners.push(fn)
}
}
removeEventListener (ev, fn) {
if (ev === 'abort') {
this._listeners = this._listeners.filter(f => f !== fn)
}
}
}
}
)

const warned = new Set()
Expand Down Expand Up @@ -721,6 +752,7 @@ class LRUCache {
deprecatedMethod('del', 'delete')
return this.delete
}

delete (k) {
let deleted = false
if (this.size !== 0) {
Expand Down Expand Up @@ -800,6 +832,7 @@ class LRUCache {
}
}
}

get reset () {
deprecatedMethod('reset', 'clear')
return this.clear
Expand All @@ -809,6 +842,10 @@ class LRUCache {
deprecatedProperty('length', 'size')
return this.size
}

static get AbortController () {
return AC
}
}

module.exports = LRUCache
41 changes: 41 additions & 0 deletions test/abort-controller.js
@@ -0,0 +1,41 @@
// this is just a test of the AbortController polyfill
// which is a little bit weird, since that's not about lru caching
// at all, so it's tempting to think that this module should
// pull it in as a dep or something. that would be the
// javascripty thing to do, right? but it would mean that
// this is no longer a zero-deps module, so meh. it's fine.
global.AbortController = null

const t = require('tap')

const LRUCache = require('../')
const { AbortController } = LRUCache

t.type(AbortController, 'function')
t.type(AbortController.AbortSignal, 'function')

t.test('onabort method', t => {
const ac = new AbortController()
t.type(ac.signal, AbortController.AbortSignal)

let calledOnAbort = false
ac.signal.onabort = () => calledOnAbort = true
ac.abort()

t.end()
})

t.test('add/remove event listener', t => {
const ac = new AbortController()
let receivedEvent = null
ac.signal.addEventListener('abort', e => receivedEvent = e)
const nope = () => { throw 'nope' }
ac.signal.addEventListener('abort', nope)
ac.signal.removeEventListener('abort', nope)
ac.signal.addEventListener('foo', nope)
ac.signal.dispatchEvent({ type: 'foo', target: ac.signal })
ac.signal.removeEventListener('foo', nope)
ac.abort()
t.match(receivedEvent, { type: 'abort', target: ac.signal })
t.end()
})
11 changes: 3 additions & 8 deletions test/fetch.js
Expand Up @@ -7,17 +7,12 @@ const clock = new Clock()
t.teardown(clock.enter())
clock.advance(1)

const LRU = require('../')

// if we're on a version that *doesn't* have a native AbortController,
// put the polyfill in there to start with, so LRU covers both cases.
global.AbortController = global.AbortController || Object.assign(
class AbortController {
constructor () { this.signal = new global.AbortController.AbortSignal }
abort () { this.signal.aborted = true }
},
{ AbortSignal: class AbortSignal { constructor () { this.aborted = false }}}
)
global.AbortController = LRU.AbortController

const LRU = require('../')
const c = new LRU({
fetchMethod: fn,
max: 5,
Expand Down

0 comments on commit 2cbea7e

Please sign in to comment.