Skip to content

Commit

Permalink
Expose Cache (#231)
Browse files Browse the repository at this point in the history
* Add CacheInterface type

* Create defautl cache and use it in the code

* Add test to custom cache support

* Replace custom cache with exposed cache

* Fix error in comment

* Update README.md (#235)

* test(typo): fix typo (#244)

* fix weakmap key is `null` (#251)

* fix race condition and add test (#261)

* 0.1.17

* Move getKeyArgs and getErrorKey to serializeKey in Cache

* Make the Map instance in Cache private

* Add methods to subscribe to cache updates and get all keys

* Apply suggestions from code review

Co-Authored-By: Shu Ding <[email protected]>

* Don't ignore tests

* Cache methods receive keyInterface and serialize it if it's not a string

* Don't expose Cache class anymore

Co-authored-by: Shu Ding <[email protected]>
Co-authored-by: Darren Jennings <[email protected]>
  • Loading branch information
3 people authored Feb 24, 2020
1 parent fd0196e commit bdb4324
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 74 deletions.
99 changes: 99 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { CacheInterface, keyInterface, cacheListener } from './types'
import { mutate } from './use-swr'
import hash from './libs/hash'

export default class Cache implements CacheInterface {
private __cache: Map<string, any>
private __listeners: cacheListener[]

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

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

set(key: keyInterface, value: any, shouldNotify = true): any {
const [_key] = this.serializeKey(key)
this.__cache.set(_key, value)
if (shouldNotify) mutate(key, value, false)
this.notify()
}

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

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

clear(shouldNotify = true) {
if (shouldNotify) this.__cache.forEach(key => mutate(key, null, false))
this.__cache.clear()
this.notify()
}

delete(key: keyInterface, shouldNotify = true) {
const [_key] = this.serializeKey(key)
if (shouldNotify) mutate(key, null, false)
this.__cache.delete(_key)
this.notify()
}

// TODO: introduce namespace for the cache
serializeKey(key: keyInterface): [string, any, 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 : ''

return [key, args, errorKey]
}

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

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

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

// Notify Cache subscribers about a change in the cache
private notify() {
for (let listener of this.__listeners) {
listener()
}
}
}
21 changes: 4 additions & 17 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,10 @@ import {
RevalidateOptionInterface,
revalidateType
} from './types'
import Cache from './cache'

// Cache
const __cache = new Map()

function cacheGet(key: string): any {
return __cache.get(key)
}

function cacheSet(key: string, value: any) {
return __cache.set(key, value)
}

function cacheClear() {
__cache.clear()
}
// cache
const cache = new Cache()

// state managers
const CONCURRENT_PROMISES = {}
Expand Down Expand Up @@ -107,8 +96,6 @@ export {
FOCUS_REVALIDATORS,
CACHE_REVALIDATORS,
MUTATION_TS,
cacheGet,
cacheSet,
cacheClear
cache
}
export default defaultConfig
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export * from './use-swr'
import { default as useSWR } from './use-swr'
export { useSWRPages } from './use-swr-pages'
export { cache } from './config'
export {
ConfigInterface,
revalidateType,
RevalidateOptionInterface,
keyInterface,
responseInterface
responseInterface,
CacheInterface
} from './types'
export default useSWR
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,16 @@ export type actionType<Data, Error> = {
error?: Error
isValidating?: boolean
}

export interface CacheInterface {
get(key: keyInterface): any
set(key: keyInterface, value: any, shouldNotify?: boolean): any
keys(): string[]
has(key: keyInterface): boolean
delete(key: keyInterface, shouldNotify?: boolean): void
clear(shouldNotify?: boolean): void
serializeKey(key: keyInterface): [string, any, string]
subscribe(listener: cacheListener): () => void
}

export type cacheListener = () => void
10 changes: 5 additions & 5 deletions src/use-swr-pages.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useState, useRef } from 'react'

import { cacheGet, cacheSet } from './config'
import { cache } from './config'
import {
pagesResponseInterface,
responseInterface,
Expand Down Expand Up @@ -100,10 +100,10 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
const pageOffsetKey = `_swr_page_offset_` + pageKey

const [pageCount, setPageCount] = useState<number>(
cacheGet(pageCountKey) || 1
cache.get(pageCountKey) || 1
)
const [pageOffsets, setPageOffsets] = useState<OffsetType[]>(
cacheGet(pageOffsetKey) || [null]
cache.get(pageOffsetKey) || [null]
)
const [pageSWRs, setPageSWRs] = useState<responseInterface<Data, Error>[]>([])

Expand Down Expand Up @@ -134,7 +134,7 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
const loadMore = useCallback(() => {
if (isLoadingMore || isReachingEnd) return
setPageCount(c => {
cacheSet(pageCountKey, c + 1)
cache.set(pageCountKey, c + 1)
return c + 1
})
}, [isLoadingMore || isReachingEnd])
Expand Down Expand Up @@ -167,7 +167,7 @@ export function useSWRPages<OffsetType = any, Data = any, Error = any>(
setPageOffsets(arr => {
const _arr = [...arr]
_arr[id + 1] = newPageOffset
cacheSet(pageOffsetKey, _arr)
cache.set(pageOffsetKey, _arr)
return _arr
})
}
Expand Down
69 changes: 21 additions & 48 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import {
} from 'react'

import defaultConfig, {
cacheGet,
cacheSet,
CACHE_REVALIDATORS,
CONCURRENT_PROMISES,
CONCURRENT_PROMISES_TS,
FOCUS_REVALIDATORS,
MUTATION_TS
MUTATION_TS,
cache
} from './config'
import hash from './libs/hash'
import isDocumentVisible from './libs/is-document-visible'
import isOnline from './libs/is-online'
import throttle from './libs/throttle'
Expand All @@ -42,41 +40,18 @@ const IS_SERVER = typeof window === 'undefined'
// useLayoutEffect in the browser.
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

// TODO: introduce namepsace for the cache
const getErrorKey = key => (key ? 'err@' + key : '')
const getKeyArgs = key => {
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 || '')
}

return [key, args]
}

const NO_DEDUPE = false

const trigger: triggerInterface = (_key, shouldRevalidate = true) => {
const [key] = getKeyArgs(_key)
// we are ignoring the second argument which correspond to the arguments
// the fetcher will receive when key is an array
const [key, , keyErr] = cache.serializeKey(_key)
if (!key) return

const updaters = CACHE_REVALIDATORS[key]
if (key && updaters) {
const currentData = cacheGet(key)
const currentError = cacheGet(getErrorKey(key))
const currentData = cache.get(key)
const currentError = cache.get(keyErr)
for (let i = 0; i < updaters.length; ++i) {
updaters[i](shouldRevalidate, currentData, currentError, NO_DEDUPE)
}
Expand All @@ -97,7 +72,7 @@ const mutate: mutateInterface = async (
_data,
shouldRevalidate = true
) => {
const [key] = getKeyArgs(_key)
const [key] = cache.serializeKey(_key)
if (!key) return

// if there is no new data, call revalidate against the key
Expand All @@ -111,7 +86,7 @@ const mutate: mutateInterface = async (
if (_data && typeof _data === 'function') {
// `_data` is a function, call it passing current cache value
try {
data = await _data(cacheGet(key))
data = await _data(cache.get(key))
} catch (err) {
error = err
}
Expand All @@ -127,8 +102,8 @@ const mutate: mutateInterface = async (
}

if (typeof data !== 'undefined') {
// update cached data
cacheSet(key, data)
// update cached data, avoid notifying from the cache
cache.set(key, data, false)
}

// update existing SWR Hooks' state
Expand Down Expand Up @@ -179,10 +154,8 @@ function useSWR<Data = any, Error = any>(
// we assume `key` as the identifier of the request
// `key` can change but `fn` shouldn't
// (because `revalidate` only depends on `key`)
const [key, fnArgs] = getKeyArgs(_key)

// `keyErr` is the cache key for error objects
const keyErr = getErrorKey(key)
const [key, fnArgs, keyErr] = cache.serializeKey(_key)

config = Object.assign(
{},
Expand All @@ -196,8 +169,8 @@ function useSWR<Data = any, Error = any>(
fn = config.fetcher
}

const initialData = cacheGet(key) || config.initialData
const initialError = cacheGet(keyErr)
const initialData = cache.get(key) || config.initialData
const initialError = cache.get(keyErr)

// if a state is accessed (data, error or isValidating),
// we add the state to dependencies so if the state is
Expand Down Expand Up @@ -282,7 +255,7 @@ function useSWR<Data = any, Error = any>(

// if no cache being rendered currently (it shows a blank page),
// we trigger the loading slow event.
if (config.loadingTimeout && !cacheGet(key)) {
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading) config.onLoadingSlow(key, config)
}, config.loadingTimeout)
Expand Down Expand Up @@ -316,8 +289,8 @@ function useSWR<Data = any, Error = any>(
return false
}

cacheSet(key, newData)
cacheSet(keyErr, undefined)
cache.set(key, newData, false)
cache.set(keyErr, undefined, false)
keyRef.current = key

// new state for the reducer
Expand Down Expand Up @@ -348,7 +321,7 @@ function useSWR<Data = any, Error = any>(
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]

cacheSet(keyErr, err)
cache.set(keyErr, err, false)
keyRef.current = key

// get a new error
Expand Down Expand Up @@ -399,7 +372,7 @@ function useSWR<Data = any, Error = any>(
// and trigger a revalidation

const currentHookData = stateRef.current.data
const latestKeyedData = cacheGet(key) || config.initialData
const latestKeyedData = cache.get(key) || config.initialData

// update the state if the key changed or cache updated
if (
Expand Down Expand Up @@ -570,8 +543,8 @@ function useSWR<Data = any, Error = any>(
// (it should be suspended)

// try to get data and error from cache
let latestData = cacheGet(key)
let latestError = cacheGet(keyErr)
let latestData = cache.get(key)
let latestError = cache.get(keyErr)

if (
typeof latestData === 'undefined' &&
Expand Down
Loading

0 comments on commit bdb4324

Please sign in to comment.