Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom cache provider #1017

Merged
merged 19 commits into from
May 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 6 additions & 94 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,8 @@
import { Cache as CacheType, Key, CacheListener } from './types'
import hash from './libs/hash'
import { Cache } from './types'

export default class Cache implements CacheType {
private cache: Map<string, any>
private subs: CacheListener[]

constructor(initialData: any = {}) {
this.cache = new Map(Object.entries(initialData))
this.subs = []
}

get(key: Key): any {
const [_key] = this.serializeKey(key)
return this.cache.get(_key)
}

set(key: Key, value: any): any {
const [_key] = this.serializeKey(key)
this.cache.set(_key, value)
this.notify()
}

keys() {
return Array.from(this.cache.keys())
}

has(key: Key) {
const [_key] = this.serializeKey(key)
return this.cache.has(_key)
}

clear() {
this.cache.clear()
this.notify()
}

delete(key: Key) {
const [_key] = this.serializeKey(key)
this.cache.delete(_key)
this.notify()
}

// TODO: introduce namespace for the cache
serializeKey(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}

if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}

const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'validating@' + key : ''

return [key, args, errorKey, isValidatingKey]
}

subscribe(listener: CacheListener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}

let isSubscribed = true
this.subs.push(listener)

return () => {
if (!isSubscribed) return
isSubscribed = false
const index = this.subs.indexOf(listener)
if (index > -1) {
this.subs[index] = this.subs[this.subs.length - 1]
this.subs.length--
}
}
}

// Notify Cache subscribers about a change in the cache
private notify() {
for (let listener of this.subs) {
listener()
}
}
export function wrapCache<Data = any>(provider: Cache<Data>): Cache {
// We might want to inject an extra layer on top of `provider` in the future,
// such as key serialization, auto GC, etc.
// For now, it's just a `Map` interface without any modifications.
return provider
}
2 changes: 1 addition & 1 deletion src/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { createContext } from 'react'
import { SWRConfiguration } from './types'

const SWRConfigContext = createContext<SWRConfiguration>({})
SWRConfigContext.displayName = 'SWRConfigContext'
SWRConfigContext.displayName = 'SWRConfig'

export default SWRConfigContext
39 changes: 23 additions & 16 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { dequal } from 'dequal/lite'
import { Configuration, RevalidatorOptions, Revalidator } from './types'
import Cache from './cache'

import { wrapCache } from './cache'
import webPreset from './libs/web-preset'
import { Configuration, RevalidatorOptions, Revalidator } from './types'

// cache
const cache = new Cache()
const fetcher = (url: string) => fetch(url).then(res => res.json())
const noop = () => {}

// error retry
function onErrorRetry(
Expand All @@ -14,7 +15,7 @@ function onErrorRetry(
revalidate: Revalidator,
opts: Required<RevalidatorOptions>
): void {
if (!config.isDocumentVisible()) {
if (!webPreset.isDocumentVisible()) {
// if it's hidden, stop
// it will auto revalidate when focus
return
Expand Down Expand Up @@ -49,28 +50,34 @@ const slowConnection =
// config
const defaultConfig = {
// events
onLoadingSlow: () => {},
onSuccess: () => {},
onError: () => {},
onLoadingSlow: noop,
onSuccess: noop,
onError: noop,
onErrorRetry,

errorRetryInterval: (slowConnection ? 10 : 5) * 1000,
focusThrottleInterval: 5 * 1000,
dedupingInterval: 2 * 1000,
loadingTimeout: (slowConnection ? 5 : 3) * 1000,

refreshInterval: 0,
// switches
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshWhenHidden: false,
refreshWhenOffline: false,
shouldRetryOnError: true,
suspense: false,
compare: dequal,

// timeouts
errorRetryInterval: (slowConnection ? 10 : 5) * 1000,
focusThrottleInterval: 5 * 1000,
dedupingInterval: 2 * 1000,
loadingTimeout: (slowConnection ? 5 : 3) * 1000,
refreshInterval: 0,

// providers
fetcher,
compare: dequal,
isPaused: () => false,
cache: wrapCache(new Map()),

// presets
...webPreset
} as const

export { cache }
export default defaultConfig
6 changes: 1 addition & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export * from './use-swr'
// `useSWRInfinite`
export { useSWRInfinite } from './use-swr-infinite'

// Cache related, to be replaced by the new APIs
export { cache } from './config'

// Types
export {
SWRConfiguration,
Expand All @@ -27,6 +24,5 @@ export {
revalidateType,
RevalidateOptionInterface,
keyInterface,
responseInterface,
CacheInterface
responseInterface
} from './types'
28 changes: 28 additions & 0 deletions src/libs/serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import hash from './hash'
import { Key } from '../types'

export function serialize(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}

if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}

const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'req@' + key : ''

return [key, args, errorKey, isValidatingKey]
}
43 changes: 18 additions & 25 deletions src/libs/web-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,46 @@
let online = true
const isOnline = () => online

// For node and React Native, `window.addEventListener` doesn't exist.
const addWindowEventListener =
typeof window !== 'undefined' ? window.addEventListener.bind(window) : null
const addDocumentEventListener =
typeof document !== 'undefined'
? document.addEventListener.bind(document)
: null

const isDocumentVisible = () => {
if (
typeof document !== 'undefined' &&
document.visibilityState !== undefined
) {
if (addDocumentEventListener && document.visibilityState !== undefined) {
return document.visibilityState !== 'hidden'
}
// always assume it's visible
return true
}

const fetcher = (url: string) => fetch(url).then(res => res.json())

const registerOnFocus = (cb: () => void) => {
if (
typeof window !== 'undefined' &&
window.addEventListener !== undefined &&
typeof document !== 'undefined' &&
document.addEventListener !== undefined
) {
if (addWindowEventListener && addDocumentEventListener) {
// focus revalidate
document.addEventListener('visibilitychange', () => cb(), false)
window.addEventListener('focus', () => cb(), false)
addDocumentEventListener('visibilitychange', () => cb())
addWindowEventListener('focus', () => cb())
}
}

const registerOnReconnect = (cb: () => void) => {
if (typeof window !== 'undefined' && window.addEventListener !== undefined) {
if (addWindowEventListener) {
// reconnect revalidate
window.addEventListener(
'online',
() => {
online = true
cb()
},
false
)
addWindowEventListener('online', () => {
online = true
cb()
})

// nothing to revalidate, just update the status
window.addEventListener('offline', () => (online = false), false)
addWindowEventListener('offline', () => (online = false))
}
}

export default {
isOnline,
isDocumentVisible,
fetcher,
registerOnFocus,
registerOnReconnect
}
4 changes: 2 additions & 2 deletions src/resolve-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function useArgs<KeyType, ConfigType, Data>(
| readonly [KeyType, Fetcher<Data> | null]
| readonly [KeyType, ConfigType | undefined]
| readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined]
): [KeyType, (typeof defaultConfig) & ConfigType, Fetcher<Data> | null] {
): [KeyType, Fetcher<Data> | null, (typeof defaultConfig) & ConfigType] {
const config = Object.assign(
{},
defaultConfig,
Expand All @@ -37,5 +37,5 @@ export default function useArgs<KeyType, ConfigType, Data>(
? args[1]
: config.fetcher) as Fetcher<Data> | null

return [args[0], config, fn]
return [args[0], fn, config]
}
Loading