Skip to content

Instantly share code, notes, and snippets.

@jamesknelson
Created November 24, 2019 04:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesknelson/f96693d47818fafebe5b52bfd5c05f83 to your computer and use it in GitHub Desktop.
Save jamesknelson/f96693d47818fafebe5b52bfd5c05f83 to your computer and use it in GitHub Desktop.
A model class for storing data fetched with React Suspense
const PurgeDelay = 1000
class Model {
fetcher: (id: string) => Promise<any>
cache = {}
callbacks = {}
holds = {}
purgeTimeouts = {}
constructor(fetcher) {
this.fetcher = fetcher
}
getCurrentValue(id) {
return this.cache[id] || { id, status: 'pending' }
}
subscribe(id, callback) {
const release = this.hold(id)
const cached = this.cache[id]
let callbacks = this.callbacks[id]
if (!callbacks) {
this.callbacks[id] = callbacks = []
}
if (callbacks.length === 0 && !cached) {
this.refresh(id)
}
callbacks.push(callback)
let isUnsubscribed = false
return () => {
if (!isUnsubscribed) {
release()
const index = callbacks.indexOf(callback)
callbacks.splice(index, 1)
if (callbacks.length === 0) {
delete this.callbacks[id]
}
isUnsubscribed = true
}
}
}
hold(id) {
const existingHolds = this.holds[id] || 0
this.holds[id] = existingHolds + 1
if (existingHolds === 0) {
this.cancelScheduledPurge(id)
}
let isUnsubscribed = false
return () => {
if (!isUnsubscribed) {
const holdsAfterRelease = --this.holds[id]
if (holdsAfterRelease === 0) {
delete this.holds[id]
this.schedulePurge(id)
}
isUnsubscribed = true
}
}
}
async refresh(id) {
const release = this.hold(id)
this.update(id, 'pending')
const cached = this.cache[id]
try {
const data = await this.fetcher(id)
if (cached === this.cache[id]) {
this.update(id, 'mirrored', data === undefined ? null : data)
}
}
catch (error) {
if (cached === this.cache[id]) {
this.update(id, 'error')
}
}
finally {
release()
}
}
update(id, status, data?) {
const currentValue = this.getCurrentValue(id)
// We don't want to notify subscribers unless something has changed
if (currentValue.status === status && currentValue.data === data) {
return
}
const resource = { id, status, data: data === undefined ? currentValue.data : data }
this.cache[id] = resource
const callbacks = this.callbacks[id]
if (callbacks) {
for (let callback of callbacks.slice(0)) {
callback(callback)
}
} else {
this.schedulePurge(id)
}
}
private cancelScheduledPurge(id) {
const purgeTimeout = this.purgeTimeouts[id]
if (purgeTimeout) {
clearTimeout(purgeTimeout)
delete this.purgeTimeouts[id]
}
}
private schedulePurge(id) {
this.cancelScheduledPurge(id)
this.purgeTimeouts[id] = setTimeout(() => {
delete this.purgeTimeouts[id]
if (!this.callbacks[id]) {
delete this.cache[id]
}
}, PurgeDelay)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment