From 2cbea7e962c53db944147a11eddbdcf717c746ab Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 26 Apr 2022 07:13:30 -0700 Subject: [PATCH] Better AbortController polyfill Still minimal, but plays nicer with things that expect to listen for 'abort' events using addEventListener. Fix: #230 --- README.md | 12 ++++++++--- index.js | 43 +++++++++++++++++++++++++++++++++++++--- test/abort-controller.js | 41 ++++++++++++++++++++++++++++++++++++++ test/fetch.js | 11 +++------- 4 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 test/abort-controller.js diff --git a/README.md b/README.md index ed4d657f..c7ebb17c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/index.js b/index.js index 9773bf0d..fb1a076f 100644 --- a/index.js +++ b/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() @@ -721,6 +752,7 @@ class LRUCache { deprecatedMethod('del', 'delete') return this.delete } + delete (k) { let deleted = false if (this.size !== 0) { @@ -800,6 +832,7 @@ class LRUCache { } } } + get reset () { deprecatedMethod('reset', 'clear') return this.clear @@ -809,6 +842,10 @@ class LRUCache { deprecatedProperty('length', 'size') return this.size } + + static get AbortController () { + return AC + } } module.exports = LRUCache diff --git a/test/abort-controller.js b/test/abort-controller.js new file mode 100644 index 00000000..6bf12a00 --- /dev/null +++ b/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() +}) diff --git a/test/fetch.js b/test/fetch.js index 1c9acd53..6d90263e 100644 --- a/test/fetch.js +++ b/test/fetch.js @@ -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,